Theming a Dojo application
Dojo applications need a way to present all the widgets they use in a consistent manner, so that users perceive and interact with application features holistically, rather than as a mashup of disjointed elements on a webpage. This is usually implemented via a corporate or product marketing theme that specifies colors, layout, font families, and more.
Making themeable widgets
There are two requirements for widgets to be considered themeable:
- The widget's factory should have the
theme
middleware injected,const factory = create({ theme })
- 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):
- 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.
theme.classes
method
The theme.classes
transforms widgets CSS class names to the application or widget's theme class names.
theme.classes<T extends ClassNames>(css: T): T;
- 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
theme
(optional)- If specified, the provided theme will act as an override for any theme that the widget may use, and will take precedence over the application's default theme as well as any other theme changes made in the application.
classes
(optional)- described in the Passing extra classes to widgets section.
Themeable widget example
Given the following CSS module file for a themeable widget:
src/styles/MyThemeableWidget.m.css
/* requirement 4, i.e. this widget is intended for wider distribution,
therefore its outer-most VDOM element uses the 'root' class: */
.root {
font-family: sans-serif;
}
/* widgets can use any variety of ancillary CSS classes that are also themeable */
.myWidgetExtraThemeableClass {
font-variant: small-caps;
}
/* extra 'fixed' classes can also be used to specify a widget's structural styling, which is not intended to be
overridden via a theme */
.myWidgetStructuralClass {
font-style: italic;
}
This stylesheet can be used within a corresponding themeable widget as follows:
src/widgets/MyThemeableWidget.tsx
import { create, tsx } from '@dojo/framework/core/vdom';
import theme from '@dojo/framework/core/middleware/theme';
import * as css from '../styles/MyThemeableWidget.m.css';
/* requirement 1: */
const factory = create({ theme });
export default factory(function MyThemeableWidget({ middleware: { theme } }) {
/* requirement 2 */
const { root, myWidgetExtraThemeableClass } = theme.classes(css);
return (
<div
classes={[
/* requirement 3: */
root,
myWidgetExtraThemeableClass,
css.myWidgetExtraThemeableClass
]}
>
Hello from a themed Dojo widget!
</div>
);
});
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 CSS module composition).
Extending the above example:
src/styles/MyThemeCommonStyles.m.css
.commonBase {
border: 4px solid black;
border-radius: 4em;
padding: 2em;
}
src/widgets/MyThemeableWidget.tsx
import { create, tsx } from '@dojo/framework/core/vdom';
import theme from '@dojo/framework/core/middleware/theme';
import * as css from '../styles/MyThemeableWidget.m.css';
import * as commonCss from '../styles/MyThemeCommonStyles.m.css';
const factory = create({ theme });
export default factory(function MyThemeableWidget({ middleware: { theme } }) {
const { root } = theme.classes(css);
const { commonBase } = theme.classes(commonCss);
return <div classes={[root, commonBase, css.myWidgetExtraThemeableClass]}>Hello from a themed Dojo widget!</div>;
});
Overriding the theme of specific widget instances
Users of a widget can override the theme of a specific instance by passing in a valid theme 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 themeable widget example:
src/themes/myTheme/styles/MyThemeableWidget.m.css
.root {
color: blue;
}
src/themes/myThemeOverride/theme.ts
import * as myThemeableWidgetCss from './styles/MyThemeableWidget.m.css';
export default {
'my-app/MyThemeableWidget': myThemeableWidgetCss
};
src/widgets/MyApp.tsx
import { create, tsx } from '@dojo/framework/core/vdom';
import MyThemeableWidget from './src/widgets/MyThemeableWidget.tsx';
import * as myThemeOverride from '../themes/myThemeOverride/theme.ts';
const factory = create();
export default factory(function MyApp() {
return (
<div>
<MyThemeableWidget />
<MyThemeableWidget theme={myThemeOverride} />
</div>
);
});
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
.
Passing extra classes to widgets
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:
- The appropriate widget theme key, specifying the widget that the classes should be applied to, including those for any child widgets that may be utilized.
- 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:
type ExtraClassName = string | null | undefined | boolean;
interface Classes {
[widgetThemeKey: string]: {
[baseClassName: string]: ExtraClassName[];
};
}
As an example of providing extra classes, the following tweaks an instance of a Dojo combobox, as well as the text input 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
.blueBackground {
background-color: blue;
}
.redArrow {
color: red;
}
src/widgets/MyWidget.tsx
import { create, tsx } from '@dojo/framework/core/vdom';
import ComboBox from '@dojo/widgets/combobox';
import * as myComboBoxStyleTweaks from '../styles/MyComboBoxStyleTweaks.m.css';
const myExtraClasses = {
'@dojo/widgets/combobox': {
controls: [myComboBoxStyleTweaks.blueBackground],
trigger: [myComboBoxStyleTweaks.redArrow]
},
'@dojo/widgets/text-input': {
input: [myComboBoxStyleTweaks.blueBackground]
}
};
const factory = create();
export default factory(function MyWidget() {
return (
<div>
Hello from a tweaked Dojo combobox!
<ComboBox classes={myExtraClasses} results={['foo', 'bar']} />
</div>
);
});
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
import { create, tsx } from '@dojo/framework/core/vdom';
import theme from '@dojo/framework/core/middleware/theme';
import myTheme from '../themes/MyTheme/theme';
const factory = create({ theme });
export default factory(function App({ middleware: { theme }}) {
// if the theme isn't set, set the default theme
if (!theme.get()) {
theme.set(myTheme);
}
return (
// the application's widgets
);
});
See Writing a theme 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 independently-distributed theme 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
@import '@{myThemePackageName}/{myThemeName}/index.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
import { create, tsx } from '@dojo/framework/core/vdom';
import theme from '@dojo/framework/core/middleware/theme';
import myTheme from '../themes/MyTheme/theme';
import alternativeTheme from '../themes/MyAlternativeTheme/theme';
const factory = create({ theme });
export default factory(function ThemeSwitcher({ middleware: { theme } }) {
return (
<div>
<button
onclick={() => {
theme.set(myTheme);
}}
>
Use Default Theme
</button>
<button
onclick={() => {
theme.set(alternativeTheme);
}}
>
Use Alternative Theme
</button>
</div>
);
});