dojo dragon main logo

Styling and theming in Dojo

Dojo widgets function best as simple components that each handle a single responsibility. They should be as encapsulated and modular as possible to promote reusability while avoiding conflicts with other widgets the application may also be using.

Widgets can be styled via regular CSS, but to support encapsulation and reuse goals, each widget should maintain its own independent CSS module that lives parallel to the widget's source code. This allows widgets to be styled independently, without clashing on similar class names used elsewhere in an application.

Dojo differentiates between several types of styling, each representing a different aspect and granularity of styling concerns within an enterprise web application:

  • Widget non-themeable styles (granularity: per-widget)
    • The minimum styles necessary for a widget to function, that are not intended to be overridden by a theme. Widgets refer to these style classes directly from their CSS module imports when rendering.
  • Widget themeable styles (granularity: per-widget)
    • Widget styles that can be overridden via theming. Widgets use the theme.classes(css) API from the theme middleware, passing in the CSS that requires theming and using the returned class names when rendering. Users of the widget can override some or all of these classes as needed.
  • Cross-cutting styles (granularity: application-wide)

As the above list illustrates, Dojo provides several complementary mechanisms for application developers to provide and override CSS styling classes, whether across an entire application or specific to individual style rules within a single styling class.

Structural widget styling

Dojo leverages CSS Modules to provide all of the flexibility of CSS, but with the additional benefit of localized classes to help prevent inadvertent styling collisions across a large application. Dojo also generates type definitions for each CSS module, allowing widgets to import their CSS similar to any other TypeScript module and refer to CSS class names in a type-safe manner, at design-time via IDE autocompletion.

The CSS module file for a widget should have a .m.css extension, and by convention is usually named the same as the widget it is associated with. Files with this extension will be processed as CSS modules rather than plain CSS files.

Example

Given the following CSS module file for a widget:

src/styles/MyWidget.m.css

.myWidgetClass {
    font-variant: small-caps;
}

.myWidgetExtraClass {
    font-style: italic;
}

This stylesheet can be used within a corresponding widget as follows:

src/widgets/MyWidget.ts

import { create, tsx } from '@dojo/framework/core/vdom';

import * as css from '../styles/MyWidget.m.css';

const factory = create();

export default factory(function MyWidget() {
    return <div classes={[css.myWidgetClass, css.myWidgetExtraClass]}>Hello from a Dojo widget!</div>;
});

When inspecting the CSS classes of these sample widgets in a built application, they will not contain myWidgetClass and myWidgetExtraClass, but rather obfuscated CSS class names similar to MyWidget-m__myWidgetClass__33zN8 and MyWidget-m__myWidgetExtraClass___g3St.

These obfuscated class names are localized to MyWidget elements, and are determined by Dojo's CSS modules build process. With this mechanism it is possible for other widgets in the same application to also use the myWidgetClass class name with different styling rules, and not encounter any conflicts between each set of styles.

Warning: The obfuscated CSS class names should be considered unreliable and may change with a new build of an application, so developers should not explicitly reference them (for examples if attempting to target an element from elsewhere in an application).

Abstracting and extending stylesheets

CSS custom properties

Dojo allows use of modern CSS features such as custom properties and var() to help abstract and centralize common styling properties within an application.

Rather than having to specify the same values for colors or fonts in every widget's CSS module, abstract custom properties can instead be referenced by name, with values then provided as a theme variant within a .root class. This separation allows for much simpler maintenance of common styling concerns across an entire application and for theme variants to be created by changing variables.

Note: do not import the theme variant file into a widget's css module; this is handled instead at run time via the theme.variant() class.

For example:

src/themes/MyTheme/variants/default.m.css

.root {
    --dark-background: black;
    --dark-foreground: lightgray;

    --padding: 32px;
}

src/themes/MyTheme/MyWidget.m.css

.root {
    margin: var(--padding);

    color: var(--dark-foreground);
    background: var(--dark-background);
}

Dojo's default build process propagates custom properties as-is into the application's output stylesheets. This is fine when only targeting evergreen browsers, but can be problematic when also needing to target browsers that do not implement the CSS custom properties standard (such as IE). To get around this, applications can be built in legacy mode (dojo build app --legacy), in which case Dojo will resolve the values of custom properties at build time and duplicate them in the output stylesheets. One value will contain the original var() reference, and the second will be the resolved value that legacy browsers can fall back to when they are unable to process the var() values.

CSS module composition

Applying a theme to a Dojo widget results in the widget's default styling classes being entirely overridden by those provided in the theme. This can be problematic when only a subset of properties in a given styling class need to be modified through a theme, while the remainder can stay as default.

CSS module files in Dojo applications can leverage composes: functionality to apply sets of styles from one class selector to another. This can be useful when creating a new theme that tweaks an existing one, as well as for general abstraction of common sets of styling properties within a single theme (note that CSS custom properties are a more standardized way of abstracting values for individual style properties).

Warning: Use of composes: can prove brittle, for example when extending a third-party theme that is not under direct control of the current application. Any change made by a third-party could break an application theme that composes the underlying theme, and such breakages can be problematic to pin down and resolve.

However, careful use of this feature can be helpful in large applications. For example, centralizing a common set of properties:

src/themes/common/ButtonBase.m.css

.buttonBase {
    margin-right: 10px;
    display: inline-block;
    font-size: 14px;
    text-align: left;
    background-color: white;
}

src/themes/myBlueTheme/MyButton.m.css

.root {
    composes: buttonBase from '../common/ButtonBase.m.css';
    background-color: blue;
}

Dojo styling best-practices

As styles in a Dojo application are mostly scoped to individual widgets, there is little need for complex selector targeting. Style application in Dojo should be as simple as possible - developers can achieve this by following a few simple recommendations:

  • Maintain encapsulated widget styling
    • A single CSS module should address a single concern. For widget-aligned modules, this usually means only including styling classes for the single accompanying widget. CSS modules can also be shared across several widgets, for example an application could define a common typography module that is shared across an application. It is common practice for widgets to reference several CSS modules within their TypeScript code.
    • Do not refer to a widget via its styling classes outside of its CSS module, or a theme that provides style overrides for the widget.
    • Do not rely on styling class names in built applications, as Dojo obfuscates them.
  • Prefer class-level selector specificity
    • Type selectors should not be used, as doing so breaks widget encapsulation and could negatively impact other widgets that use the same element types.
    • ID selectors should not be used. Dojo widgets are intended to be encapsulated and reusable, whereas element IDs are contrary to this goal. Dojo provides alternative mechanisms to augment or override styles for specific widget instances, such as via a widget's classes or theme properties.
  • Avoid selector nesting
    • Widgets should be simple enough to only require single, direct class selectors. If required, widgets can use multiple, independent classes to apply additional style sets. A single widget can also use multiple classes defined across several CSS modules.
    • Complex widgets should be refactored to a simple parent element that composes simple child widgets, where specific, encapsulated styling can be applied to each composed widget.
  • Avoid BEM naming conventions
    • Favor descriptive class names relevant to the widget's purpose.
  • Avoid use of !important