Theming a Dojo application

    There are two requirements for widgets to be considered themeable:

    1. The widget’s factory should have the theme middleware injected, const factory = create({ theme })
    2. One or more of the widget’s styling classes should be passed using the result from the theme.classes(css) call when rendering the widget.

    By convention, there is a third requirement that is useful when developing widgets intended for distribution (this is a convention that widgets in Dojo’s widget library follow):

    1. The widget’s root VDOM node - that is, the outer-most node rendered by the widget - should include a styling class named root. Doing so provides a predictable way to target the top-level node of a third-party themeable widget when overriding its styles in a custom theme.

    The theme middleware is imported from the @dojo/framework/core/middleware/theme module.

    The theme.classes transforms widgets CSS class names to the application or widget’s theme class names.

    • Note 1: Theme overrides are at the level of CSS classes only, not individual style properties within a class.
    • Note 2: If the currently active theme does not provide an override for a given styling class, the widget will fall back to using its default style properties for that class.
    • Note 3: If the currently active theme does provide an override for a given styling class, the widget will only have the set of CSS properties specified in the theme applied to it. For example, if a widget’s default styling class contains ten CSS properties but the current theme only specifies one, the widget will render with a single CSS property and lose the other nine that were not specified in the theme override.

    theme middleware properties

    Given the following CSS module file for a themeable widget:

    1. /* requirement 4, i.e. this widget is intended for wider distribution,
    2. therefore its outer-most VDOM element uses the 'root' class: */
    3. .root {
    4. font-family: sans-serif;
    5. }
    6. /* widgets can use any variety of ancillary CSS classes that are also themeable */
    7. .myWidgetExtraThemeableClass {
    8. font-variant: small-caps;
    9. }
    10. /* extra 'fixed' classes can also be used to specify a widget's structural styling, which is not intended to be
    11. overridden via a theme */
    12. .myWidgetStructuralClass {
    13. font-style: italic;
    14. }

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

    src/widgets/MyThemeableWidget.tsx

    1. import { create, tsx } from '@dojo/framework/core/vdom';
    2. import theme from '@dojo/framework/core/middleware/theme';
    3. import * as css from '../styles/MyThemeableWidget.m.css';
    4. /* requirement 1: */
    5. const factory = create({ theme });
    6. export default factory(function MyThemeableWidget({ middleware: { theme } }) {
    7. /* requirement 2 */
    8. const { root, myWidgetExtraThemeableClass } = theme.classes(css);
    9. return (
    10. <div
    11. classes={[
    12. /* requirement 3: */
    13. root,
    14. myWidgetExtraThemeableClass,
    15. css.myWidgetExtraThemeableClass
    16. ]}
    17. Hello from a themed Dojo widget!
    18. </div>
    19. );
    20. });

    Using several CSS modules

    Widgets can also import and reference multiple CSS modules - this provides another way to abstract and reuse common styling properties through TypeScript code, in addition to the CSS-based methods described elsewhere in this guide (CSS custom properties and ).

    Extending the above example:

    src/styles/MyThemeCommonStyles.m.css

    1. .commonBase {
    2. border: 4px solid black;
    3. border-radius: 4em;
    4. padding: 2em;

    Overriding the theme of specific widget instances

    Users of a widget can override the theme of a specific instance by passing in a to the instance’s theme property. This is useful when needing to display a given widget in multiple ways across several occurrences within an application.

    For example, building on the :

    1. .root {
    2. color: blue;
    3. }

    src/themes/myThemeOverride/theme.ts

    1. import * as myThemeableWidgetCss from './styles/MyThemeableWidget.m.css';
    2. export default {
    3. 'my-app/MyThemeableWidget': myThemeableWidgetCss
    4. };

    src/widgets/MyApp.tsx

    1. import { create, tsx } from '@dojo/framework/core/vdom';
    2. import MyThemeableWidget from './src/widgets/MyThemeableWidget.tsx';
    3. import * as myThemeOverride from '../themes/myThemeOverride/theme.ts';
    4. const factory = create();
    5. export default factory(function MyApp() {
    6. return (
    7. <div>
    8. <MyThemeableWidget />
    9. <MyThemeableWidget theme={myThemeOverride} />
    10. </div>
    11. );
    12. });

    Here, two instances of MyThemeableWidget are rendered - the first uses the application-wide theme, if specified, otherwise the widget’s default styling is used instead. By contrast, the second instance will always render with the theme defined in myThemeOverride.

    The theming mechanism provides a simple way to consistently apply custom styles across every widget in an application, but isn’t flexible enough for scenarios where a user wants to apply additional styles to specific instances of a given widget.

    Extra styling classes can be passed in through a themeable widget’s classes property. They are considered additive, and do not override the widget’s existing styling classes - their purpose is instead to allow fine-grained tweaking of pre-existing styles. Each set of extra classes provided need to be grouped by two levels of keys:

    1. The appropriate , specifying the widget that the classes should be applied to, including those for any child widgets that may be utilized.
    2. Specific existing CSS classes that the widget utilizes, allowing widget consumers to target styling extensions at the level of individual DOM elements, out of several that a widget may output.

    For illustration, the type definition for the extra classes property is:

    As an example of providing extra classes, the following tweaks an instance of a Dojo combobox, as well as the child widget it contains. This will change the background color to blue for both the text input control used by the combobox as well as its control panel. The down arrow within the combo box’s control panel will also be colored red:

    src/styles/MyComboBoxStyleTweaks.m.css

    1. .blueBackground {
    2. background-color: blue;
    3. }
    4. .redArrow {
    5. color: red;
    6. }
    1. import { create, tsx } from '@dojo/framework/core/vdom';
    2. import ComboBox from '@dojo/widgets/combobox';
    3. import * as myComboBoxStyleTweaks from '../styles/MyComboBoxStyleTweaks.m.css';
    4. const myExtraClasses = {
    5. '@dojo/widgets/combobox': {
    6. controls: [myComboBoxStyleTweaks.blueBackground],
    7. trigger: [myComboBoxStyleTweaks.redArrow]
    8. },
    9. '@dojo/widgets/text-input': {
    10. input: [myComboBoxStyleTweaks.blueBackground]
    11. }
    12. const factory = create();
    13. export default factory(function MyWidget() {
    14. return (
    15. <div>
    16. Hello from a tweaked Dojo combobox!
    17. <ComboBox classes={myExtraClasses} results={['foo', 'bar']} />
    18. );
    19. });

    Note that it is a widget author’s responsibility to explicitly pass the classes property to all child widgets that are leveraged, as the property will not be injected nor automatically passed to children by Dojo itself.

    Making themeable applications

    In order to specify a theme for all themeable widgets in an application, the theme.set API from the theme middleware can be used in the application’s top level widget. Setting a default or initial theme can be done by checking theme.get before calling theme.set.

    For example, specifying a primary application theme:

    src/App.tsx

    1. import { create, tsx } from '@dojo/framework/core/vdom';
    2. import theme from '@dojo/framework/core/middleware/theme';
    3. import myTheme from '../themes/MyTheme/theme';
    4. const factory = create({ theme });
    5. export default factory(function App({ middleware: { theme }}) {
    6. // if the theme isn't set, set the default theme
    7. if (!theme.get()) {
    8. theme.set(myTheme);
    9. }
    10. return (
    11. // the application's widgets
    12. );
    13. });

    See for a description of how the myTheme import should be structured.

    Note that using themeable widgets without having an explicit theme (for example, not setting a default theme using theme.set and not explicitly overriding a widget instance’s theme or styling classes) will result in each widget using its default style rules.

    If using an in its entirety, applications will also need to integrate the theme’s overarching index.css file into their own styling. This can be done via an import in the project’s main.css file:

    src/main.css

    By contrast, another way of using only portions of an externally-built theme is via theme composition.

    Changing the currently active theme

    The theme middleware .set(theme) function can be used to change the active theme throughout an application. Passing the desired theme to .set, which will invalidate all themed widgets in the application tree and re-render them using the new theme.

    src/widgets/ThemeSwitcher.tsx

    1. import { create, tsx } from '@dojo/framework/core/vdom';
    2. import theme from '@dojo/framework/core/middleware/theme';
    3. import myTheme from '../themes/MyTheme/theme';
    4. import alternativeTheme from '../themes/MyAlternativeTheme/theme';
    5. const factory = create({ theme });
    6. export default factory(function ThemeSwitcher({ middleware: { theme } }) {
    7. return (
    8. <div>
    9. <button
    10. onclick={() => {
    11. theme.set(myTheme);
    12. }}
    13. >
    14. Use Default Theme
    15. </button>
    16. <button
    17. onclick={() => {
    18. theme.set(alternativeTheme);
    19. }}
    20. >
    21. Use Alternative Theme
    22. </button>
    23. </div>