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 thetheme
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.
- Widget styles that can be overridden via theming. Widgets use the
- Cross-cutting styles (granularity: application-wide)
- Styles that apply across several widgets, whether widgets of different types, or multiple instances of a single widget type. These styles usually provide a consistent visual presentation for all themeable widgets used within an application. Cross-cutting styles can be provided/referenced via several mechanisms:
- Providing an application-wide theme
- Specifying per-widget themes
- Passing extra classes to a widget
- Defining css properties within a variant module and using them throughout widget stylesheets to aid consistency and theme variant creation
- Composing classes within a CSS module.
- Using several CSS modules within a widget.
- Styles that apply across several widgets, whether widgets of different types, or multiple instances of a single widget type. These styles usually provide a consistent visual presentation for all themeable widgets used within an application. Cross-cutting styles can be provided/referenced via several mechanisms:
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
ortheme
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