dojo dragon main logo

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:

  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.

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

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,
				theme.variant()
			]}
		>
			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, theme.variant()]}>
			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:

  1. 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.
  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:

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>
	);
});