Theming
Introduction
When building your own theme, it’s important to understand how to leverage the many parts of Enact so you get the most out of your effort, get something that’s easy to maintain, and are able to extend it over time. We’ve built a bunch of useful things for achieving this, so let’s start to learn about them!
Writing Customizable Components
publicClassNames
Components built with the kind
feature can use the publicClassNames
key in the styles
block of their component definition. This key allows a component to define an array of CSS class names that will be available for a component consumer to add styling to. For brevity and convenience, if you simply specify true
as the value (publicClassNames: true
), every class from your CSS file will be exported and available. All components in @enact/ui
and many in @enact/sandstone
and other themes have already been imbued with this feature, which allows direct access to customize that component’s appearance.
Sometimes, behavior is built into a component, but no visual qualities are assigned to that behavior. For example, Enact handles the selected
state of a Button
, and it’s implemented in our ui
package, so it’s universally available to all themes. ui
has no opinion on how this state is visually represented, so it simply exports a blank class that the consuming theme can style to its liking.
Here’s a simplified example of ui/Button/Button.module.less
:
// Button.module.less
//
@import "../styles/mixins.less";
.button {
display: inline-block;
position: relative;
// The background element of the Button, used on a child of the base element
.bg {
.position(0);
position: absolute;
}
.icon,
&.large,
&.small,
// The selected state of the Button, applied to the base element
&.selected {
/* Public Class Names */
}
}
There is a small caveat here, which is that classes will not be exported if they do not have something in their rule-set, like the .selected
class. It must have that CSS comment /* comment */
in place for the minifier to not prune it out. Also, a LESS style comment //
is not sufficient, since those are pruned out at compile-time.
In the above example, we define our base component class .button
, then set up child element classes: .bg
and .icon
, we also add state classes: .selected
, large
, and .small
. Because this is in our ui
package, where all of our base components are, we want to establish the common behaviors, but not the appearance, so .icon
, .selected
, large
, and .small
have no rules on them, just a CSS comment. This way, they will be available to the theme and it can choose how to visually represent those states, and not worry about the logic of how those are applied.
Using a UI Component
Let’s look at an example, Sandstone’s Button
:
import kind from '@enact/core/kind';
import UiButton from '@enact/ui/Button';
import componentCss from './Button.module.less';
const Button = kind({
name: 'CustomizedButton',
styles: {
css: componentCss,
className: 'button',
publicClassNames: ['button', 'bg', 'large', 'selected', 'small']
},
render: ({children, css, ...rest}) => (
<UiButton {...rest} css={css}>{children}</UiButton>
)
});
export default Button;
In this example, we’ve imported the unstyled ui/Button
, which exports all available classes for customization (by setting publicClassNames: true
), and we’ve imported our LESS file where we have many styles defined for the available states that a button can be in: large, small, selected, pressed, disabled, etc. We also have access to the internal element classes: bg, client, and marquee. Each of these classes match one of the public class names made available by the ui/Button
component. So what can we do with this? We, as the component authors, have access to all of the class names which were made available in the components we’re using, however, we can choose to restrict the available classes being made available to our consumers. In the above example we’ve only chosen to export four (4) classes for customization. It is the theme’s discretion to determine how customizable or rigid it should be. Sandstone, for example, is relatively rigid so it maintains consistency for its visual identity. Our ui
package, on the other hand, is completely open to customization and expresses only minimal initial styling. It makes prolific use of publicClassNames: true
, which is shorthand for allowing all classes to be customized.
Adding State Classes
Sandstone also adds its own props, which are specific to Sandstone; things like the backgroundOpacity
prop and the color
prop. They are added to the example above via the computed
block:
computed: {
className: ({backgroundOpacity, color, styler}) => styler.append(
backgroundOpacity,
color
)
},
These props are sent to our styler.append
feature, which in this case, takes the value of these prop variables and applies them directly as class names. Setting <Button color="red">
appends the .red
class to the component. If you were wanting a Boolean state class, something like the following would do the job:
computed: {
className: ({backgroundOpacity, color, ready, styler}) => styler.append(
backgroundOpacity,
color,
{
ready
}
)
},
This would apply the .ready
class, only if the ready
prop was truthy, like so: <Button color="red" ready>
and not in this this case: <Button color="red">
.
How It Works
When creating customizable components it may be helpful to understand how the theming system passes information between layers and how the internals work.
Importing
When a LESS or CSS file is imported, the classes are inventoried and a hash map is generated of original class names to obfuscated modularized class names: {original: obfuscated}
. Your module now has a map of all of the class names you referenced. Normally, when using kind()
you simply pass this into the styles
block, and indicate which one is your base class, with the className
key.
import css from './Button.module.less';
...
{
css, // Via ES6, the `css` variable is converted to {'css': css}
className: 'button'
},
...
Here, kind()
handles the mapping from the css
hash to your 'button'
class name. A similar process happens when you include the publicClassNames
key and array in this block. The names of your classes are mapped to the previously published class names, and they are paired up.
For example, Button
publishes the .bg
class from ui
. Sandstone adds its own styling rules to the existing .bg
class simply by giving its custom rules the same name as ui
. Internally, Enact is attaching both the ui
and sandstone
classes together for the .bg
original class name key, like: {bg: 'ui_Button_bg sandstone_Button_bg'}
. Excluding a class name from the publicClassNames
array will ignore that class when they’re appended to the map.
The css
prop
You can access the full collection of mapped class names via the automatically added css
prop. The css
prop doesn’t behave like normal props, though. It does not automatically pass down to deeper layers, via props-spreading. This is because we wanted to be conscious of when we are passing classes down to another layer. When you add a publicClassNames
key to your component, it will automatically receive the css
prop in its collection of props, which can be used in the render
or computed
functions just like any other prop. To reference an earlier example, when sandstone/Button
encounters the css
prop, it is an object like this:
{
button: 'ui_Button_button sandstone_Button_button',
bg: 'ui_Button_bg sandstone_Button_bg'
}
You then simply use this object to refer to your internal elements, like this:
render({children, css, ...rest}) =>
<div {...rest}>
<div className={css.bg}></div>
{children}
</div>
)
Conveniently, the base class name is applied automatically to the className
prop, which is passed through the rest
spread above. This is also true for the backgroundOpacity
and color
prop classes applied during the computed className
function.
Combining all the bits
Let’s look at our combined example again.
import kind from '@enact/core/kind';
import UiButton from '@enact/ui/Button';
import componentCss from './Button.module.less';
const Button = kind({
name: 'CustomizedButton',
styles: {
css: componentCss,
className: 'button',
publicClassNames: ['button', 'bg', 'large', 'selected', 'small']
},
render: ({children, css, ...rest}) => (
<UiButton {...rest} css={css}>{children}</UiButton>
)
});
export default Button;
We’re importing our CSS with a non-conflicting name (componentCss
); sending it into kind()
; setting our base class name, which we could have named differently than our ui
component base class name; adding some classes to be published; then extracting the automatically added css
prop down in render
; and finally, forwarding it to the UiButton
component, which publishes all of its class names for customization. And there we have a fully styled, customized Button.
Theme Creation Concepts
So, you’re fully versed in the theming system and you’re ready to make your own themed set of components, or want to create a new base component that can be themed. Let’s cover some concepts that will make your project easier to maintain.
Visually Divorced
All of the Enact components follow a naming strategy that we call “visually divorced”. This is the concept of naming all APIs, classes, and components with words that do not relate to visual concepts, but rather to semantic concepts. This way, when a theme chooses to visually represent something, like a state or a prop that we added to a component, it can fully decide how that looks, and not be dependent on how we’ve named something. As a contrived example, think about it like the shift in HTML from using <b>
bold and <i>
italic tags to <strong>
and <em>
tags. The former describe how the components visually appear, while the latter describe what the meaning of those is, which just happen to be represented by bold text and italic text by the browser. A web developer could choose to represent those concepts differently on their site, for visually impaired users, or for languages or fonts that are unable to represent a bold or italic font style.
States
Typically, we’ll represent the state of a component using a class. The presence of the class means “on” and the absence of it means “off”. This works really well for boolean properties, but can also be used to represent enumerated properties. The orientation
prop is a good example of this. A Slider can choose an orientation, which applies one of the allowed values as a class. sandstone/Slider
currently supports orientation="horizontal"
and orientation="vertical"
. These values are applied literally to the component as classes .horizontal
and .vertical
. That being said, there’s no reason why new orientations like “radial” or “2d” couldn’t be implemented, which we can then fully style using CSS however we want; maybe “radial” describes a speedometer-like dial and “2d” describes a graph-like square. Slider
imposes no expectations on how the values it manages are used, it just makes them available. The visual designer is then able to use this available information to design, restriction-free, how ever is most appropriate for their theme.
Something like the following is easy to read, understand, maintain, and extend:
.slider {
&.horizontal {
max-width: 500px;
height: 12px;
}
&.vertical {
max-height: 500px;
width: 12px;
}
&.radial {
height: 300px;
width: 300px;
border-radius: 150px;
}
}
Meanwhile, thanks to kind()
, the code to implement multiple formats is as simple as the following:
const Slider = kind({
name: 'Slider',
propTypes: {
orientation: PropTypes.oneOf(['horizontal', 'vertical', 'radial'])
},
defaultProps: {
orientation: 'horizontal'
},
computed: {
className: ({orientation, styler}) => styler.append(orientation)
},
...
Base Components
A compelling base component, one that is usable by multiple themes, will do its best to impose as few visual rules as is possible while offering hooking-points for creative visual designers to do interesting things with. You’ll want to balance what is minimally required to make the component functional, with visuals. Style rules that may be useful for functionality are things like display
type: inline, inline-block, block, flex, etc, or position
on a child element that allow it to fit to the arbitrary shape of its parent.
Component Overrides
Conventionally, the base element is overridable using the prop component
, and others via prefixing that word, e.g. iconComponent
, buttonComponent
, marqueeComponent
. This gives the theme the ability to provide its own customized drop-in component. You can assign the default in the defaultProps
config. It’s also great practice to use some low-overhead basic DOM element for the default too, to reduce loading cost, something like 'div'
, 'span'
, 'img'
, or even 'section'
.
Enact includes a great feature for merging author and consumer props onto one component to make this process much simpler. Below is an example of how ui/IconButton
is composed:
import ComponentOverride from '@enact/ui/ComponentOverride';
...
defaultProps: {
buttonComponent: 'div',
iconComponent: 'span'
},
...
render: ({buttonComponent, children, css, icon, iconComponent: Icon, size, ...rest}) => (
<ComponentOverride
{...rest}
component={buttonComponent}
size={size}
minWidth={false}
>
<Icon size={size} className={css.icon}>{icon}</Icon>
{children}
</ComponentOverride>
)
The remainder of the props are spread onto the buttonComponent
while the size
and minWidth
props are added directly. (size
could have been passed via rest
but it is also sent to Icon
, and we chose this approach to improve readability, rather than sending size={rest.size}
.)
Best practices
Should
Recommendations for components:
- Establish the complete HTML DOM structure necessary for operation, with the minimum styling needed.
- Declare and export all state-classes via
publicClassNames: true
. - Allow component and possibly sub-component (child component) overrides where reasonable.
Should Not
Generally avoid doing these for components:
- Include HOCs (higher-order components), not allowing the theme to compose and decorate its own features, unless they are always necessary for function.
- Define colors, measurements (that aren’t critical for functionality), margins/paddings, or other purely visual aspects of the component’s styles.
Creating Theme via Enact Template
You can create your own theme by using @enact/template-theme
a template generator for @enact/cli
to create Enact-based theme libraries.
Installation
The Enact CLI itself can install this via NPM or git URI.
enact template install @enact/template-theme
Usage
Once installed, you can specify the theme
template in the creation command:
enact create -t theme uranium
This will create a new copy of the starter theme in the uranium
directory. All references to the original theme’s name, LESS variables, etc. will be updated to the appropriate values for uranium
.
The starter theme comes with one skin, default-skin
, and this can be updated on creation as well by passing the --skin
flag:
enact create -t theme uranium --skin proton
Either way, the new theme is ready to be included as a dependency for your app. Its aptly named README contains other valuable information on modifying the theme, so be sure to give it a look.