Theming
Overview
This tutorial will extend on previous tutorials where we created the basic biz-e-corp application. In this tutorial, we will adapt the app to allow it to be themed, then write a theme and apply it to our application. This will be done using the built-in theming system that is included in Dojo.
Prerequisites
You can download the demo project and run npm install
to get started.
The @dojo/cli
command line tool should be installed globally. Refer to the Dojo local installation article for more information.
You also need to be familiar with TypeScript as Dojo uses it extensively.
Theming our widgets
Theming our widgets
In Dojo, we differentiate between two types of styles:
- Structural styles: these are the minimum necessary for a widget to function
- Visual styles: these are themed
The current CSS in the example app provides the structural styles, we will now review how to create and manage themes.
In order to theme our widgets, we must ensure that they each apply the ThemedMixin
and change the class name of the widget’s top-level node to root
. The ThemedMixin
provides a this.theme
function that returns (or that will return) the correct class for the theme
provided to the widget. The top-level class name is changed to root
in order to provide a predictable way to target the outer-node of a widget, this is a pattern used throughout widgets provided by @dojo/widgets
.
Replace the contents of Banner.ts
with the following
File: src/widgets/Banner.tsimport { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
import { v } from '@dojo/framework/widget-core/d';
import { theme, ThemedMixin } from '@dojo/framework/widget-core/mixins/Themed';
import * as css from '../styles/banner.m.css';
@theme(css)
export default class Banner extends ThemedMixin(WidgetBase) {
protected render() {
return v('h1', {
title: 'I am a title!',
classes: this.theme(css.root)
}, [ 'Biz-E-Bodies' ]);
}
}
Reminder
If you cannot see the application, remember to run dojo build -m dev -w -s
to build the application and start the development server.
This Banner
widget will now have access to the classes in banner.m.css
and can receive a theme
. We use the root
class to ensure that the theme we create can easily target the correct node.
Notice that this file and other css
files now have a .m.css
file extension. This is to indicate to the Dojo build system that this file is a css-module
and should be processed as such.
CSS Modules
CSS Modules is a technique to use scoped CSS classnames by default.
Create a new style sheet for the Banner
widget named banner.m.css
.
We will create an empty root
class for now as our base theme does not require any styles to be added to the Banner
widget.
File: src/styles/banner.m.css.root {
}
Now, let's look at changing the WorkerForm
widget
Fixed Classes
Fixed classes apply styles that cannot be overridden by a theme, using a suffix is a convention that helps easily differentiate the intention of these classes.
WorkerForm
already uses the ThemedMixin
and has a workerForm
class on its root node. Let’s change the workerForm class to a root
class, and while we are there, we will create a rootFixed
class too, and apply it to the root node. Classes that are not passed to theme
cannot be changed or overridden via a theme, ensuring that structured or nested styles are not lost when a theme is used.
File: src/widgets/WorkerForms.tsreturn v('form', {
classes: [ this.theme(css.root), css.rootFixed ],
onsubmit: this._onSubmit
}, [
Replace all of the selectors containing .workerForm
with the following rules in workerForm.m.css
.
File: src/styles/workerForm.m.css.root {
margin-bottom: 40px;
}
.rootFixed {
text-align: center;
}
.rootFixed fieldset,
.rootFixed label {
display: inline-block;
text-align: left;
}
.rootFixed label {
margin-right: 10px;
}
If you open the application in a browser its appearance and behavior should be unchanged.
Now update worker.m.css
and workerContainer.m.css
to use .root
and .rootFixed
…
File: src/styles/workerContainer.m.css.rootFixed {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: stretch;
margin: 0 auto;
width: 100%;
}
.root {
}
File: src/styles/worker.m.css..root {
margin: 0 10px 40px;
max-width: 350px;
min-width: 250px;
flex: 1 1 calc(33% - 20px);
position: relative;
}
.rootFixed {
/* flip transform styles */
perspective: 1000px;
transform-style: preserve-3d;
}
All rules were originally in the .worker selector.
…and then update the associated widgets to use the new selectors.
// WorkerContainer.ts
// ...
render() {
// ...
return v('div', {
classes: [ this.theme(css.root), css.rootFixed ]
}, workers);
// ...
}
// Worker.ts
// ...
render() {
// ...
return v('div', {
classes: [
...this.theme([ css.root, this._isFlipped ? css.reverse : null ]),
css.rootFixed
]
}, [
// ...
}
Next, we will start to create a theme.
Creating a Theme
Create a theme directory
Dojo provides a CLI command for creating a skeleton theme from existing Dojo widgets. To do this the command prompts the user to enter the name of the package that contains Dojo widgets and allows you to select specific widgets to include in the output.
At the command line run dojo create theme —name dojo
When prompted for the package to theme enter @dojo/widgets
and then type N
to indicate their are no more packages. You should now be presented with a list of widgets from @dojo/widgets that can be scaffolded. For this demo we need to select button
and text-input
using the arrow keys and space to select each one, press enter to complete the process.
This should have created a themes
directory in the project’s src
directory, this will be where the custom theme is created.
Dojo uses the concept of a key
to be able to look up and load classnames for a theme, it is a composite of the package name and the widget name joined by /
. These keys are used as the keys to object that is exported from the main theme.ts
.
Theme the Worker widget
In order to theme the Worker
widget, we need to create worker.m.css
within our themes/dojo
directory and use it within theme.ts
. As mentioned above, the naming of the key of the exported theme must match the name from the project’s package.json
and the name of the widget’s style sheet joined by a forward slash (/
).
Let’s start by creating a red Worker
and observe our theme being applied correctly.
/* worker.m.css */
.root {
background: red;
}
So for theme.ts
, you need to add the import for the worker.m.css
and the theme itself to the exported object using the key biz-e-corp/worker
.
File: src/themes/dojo/theme.tsimport * as worker from './worker.m.css';
import * as textInput from './@dojo/widgets/text-input/text-input.m.css';
import * as button from './@dojo/widgets/button/button.m.css';
export default {
'dojo-theming-tutorial/worker': worker,
'@dojo/widgets/text-input': textInput,
'@dojo/widgets/button': button
};
To apply a theme to a widget, simply pass the theme
as a property to widgets to have the ThemedMixin
applied. To ensure that the entire application applies the theme
it needs to be passed to all the themed widgets in our application. This can become problematic when an application uses a mixture of themed and non-themed widgets, or uses a widget from a third party, meaning that there is no guarantee that the theme
will be propagated as required.
What is a registry?
A registry provides a mechanism to inject external payloads into widgets throughout the application tree. To learn more, take a look at the container tutorial and registry tutorial.
However an application can automatically inject a theme
from the registry
to every themed widget in an application tree. First, create a themeInjector
using the registerThemeInjector
function by passing a registry
instance and theme
. This will return a handle to the themeInjector
that can be used to change the theme using themeInjector.set()
, which will invalidate all themed widgets in the application tree and re-render using the new theme!
Update our main.ts
file to import our theme and create a themeInjector
.
File: src/main.tsimport renderer from '@dojo/framework/widget-core/vdom';
import { w } from '@dojo/framework/widget-core/d';
import { registerThemeInjector } from '@dojo/framework/widget-core/mixins/Themed';
import { Registry } from '@dojo/framework/widget-core/Registry';
import App from './widgets/App';
import theme from './themes/dojo/theme';
const registry = new Registry();
registerThemeInjector(theme, registry);
const r = renderer(() => w(App, {}));
r.mount({ domNode: document.querySelector('my-app') as HTMLElement, registry });
Open the application in your web browser and see that the Worker
backgrounds are now red
.
Use variables and complete the Worker theme
The Dojo build system supports new CSS features such as css-custom-properties
by using PostCSS to process our .m.css
files. We can use these new CSS features to add variables to worker.m.css
and complete its theme.Let’s create themes/dojo/variables.css
(notice that this file does not have a .m.css
extension as it is not a css-module
file).
File: src/themes/dojo/variables.css:root {
--font: 'arial';
--container: #f9f9f9;
--body: #000;
--card: #fff;
--accent: #0071f2;
--secondary-accent: #075cbe;
--component-accent: #f4f4f4;
--input-border: #d6dde3;
--padding: 32px;
--spacing: 16px;
}
In the above code you can see that we have created a number of CSS Custom Properties to be used within our theme and wrapped them in a :root
selector which makes them available on the global scope within our css-modules
.To use them, we can @import
the variables.css
file and use the var
keyword to assign a css-custom-property
to a css rule.
Now we will use these variables in our themes worker.m.css
to create our fully themed Worker
.
File: src/themes/dojo/worker.m.css@import './variables.css';
.root {
width: 320px;
height: 370px;
box-sizing: border-box;
margin: var(--spacing);
position: relative;
}
.image {
background: var(--component-accent);
border-radius: 50%;
width: 250px;
height: 250px;
margin-bottom: var(--padding);
}
.imageSmall {
background: var(--component-accent);
border-radius: 50%;
width: 72px;
height: 72px;
display: inline-block;
margin-right: 24px;
}
.workerFront, .workerBack {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 1px solid var(--component-accent);
}
.workerFront {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.35);
padding: var(--padding);
text-align: center;
}
.workerBack {
box-shadow: 0 20px 35px 0 rgba(0, 0, 0, 0.15);
background: var(--card);
color: var(--body);
}
.heading {
background: var(--accent);
color: var(--card);
padding: 24px;
}
.generalInfo {
display: inline-block;
vertical-align: top;
line-height: 24px;
}
.generalInfo .label {
display: none;
}
.tasks strong {
background: var(--secondary-accent);
color: var(--card);
display: block;
font-size: 14px;
padding: 14px 24px;
text-transform: uppercase;
font-weight: 400;
}
.task {
padding: 12px 24px;
border-bottom: 1px solid var(--component-accent);
}
Theming Dojo widgets
Thus far in this tutorial, we have themed our custom Worker
widget, but we have not targeted Dojo widgets that are contained within our application. To demonstrate the styling of Dojo widgets, we will theme the workerForm
widget as it contains both DOM nodes and Dojo widgets.
Let's create workerForm.m.css
File: src/themes/dojo/workerForm.m.css@import './variables.css';
.root {
font-family: var(--font);
padding: 30px;
box-sizing: border-box;
font-weight: bold;
}
.nameLabel {
font-size: 14px;
}
And include it in theme.ts
File: src/themes/dojo/theme.tsimport * as worker from './worker.m.css';
import * as workerForm from './workerForm.m.css';
export default {
'dojo-theming-tutorial/worker': worker,
'dojo-theming-tutorial/workerForm': workerForm,
};
This should be familiar from theming the Worker
in the previous section. To theme the Dojo TextInput
within our WorkerForm
, we need to add to the skeleton text-input.m.css
theme created by @dojo/cli-create-theme
, this is already exported from theme.ts
.
File: src/themes/dojo/@dojo/widgets/text-input/text-input.m.css@import './../../../variables.css';
.root {
margin-right: 10px;
display: inline-block;
composes: nameLabel from './../../../workerForm.m.css';
text-align: left;
}
.input {
height: 38px;
border: 1px solid var(--input-border);
box-sizing: border-box;
border-bottom-color: color(var(--input-border) blackness(60%));
padding: 8px;
min-width: 230px;
}
Notice the styling rule for the .root
selector? Here we are introducing another powerful part of the Dojo theming system, composes
. Composes originates in the css-module
specification, allowing you to apply styles from one class selector to another. Here we are specifying that the root
of a TextInput
(the label text in this case), should appear the same as the nameLabel
class in our WidgetForm
. This approach can be very useful when creating multiple themes from a baseTheme
and avoids repetitive redefinition of style rules.
In your web browser you will see the TextInput
widgets at the top of the form have been styled.
Add the following theme styles to the button.m.css
theme resource
File: src/themes/dojo/@dojo/widgets/button/button.m.css@import './../../../variables.css';
.root {
height: 38px;
border: 1px solid var(--input-border);
background: var(--card);
color: var(--accent);
font-size: 16px;
font-weight: bold;
padding: 8px;
min-width: 150px;
vertical-align: bottom;
}
Summary
In this tutorial, we learned:
- How to create a theme
- How to apply a theme to both Dojo and custom widgets using the
themeInjector
. - How to leverage functionality from the Dojo theming system to apply variables
- How to use
composes
to share styles between components
You can download the completed demo application from this tutorial.