Developers
On this page
Using the theming system
RHDS’s theming system is a high-level expression of the lower-level components of the system: tokens, and elements. In turn, it factors into the development of the highest-level design system component: patterns. To use the theming system, then, developers must already be familiar with our tokens and elements. In other words, theming is the developer’s process of orchestrating design tokens with elements, particularly by way of themeable container elements.
Themeable containers
In RHDS, special provider elements such as <rh-surface>
,
<rh-card>
, <rh-tabs>
, and others are considered themeable containers. A
themeable container is an element that can have custom classes attached, which
provide values for the relevant CSS color properties in a custom
theme.
A common pattern for a themeable container is the full-width band. For example,
a <rh-surface>
may be used as a full-width container and provide the
Bordeaux theme values to a set of 3 cards in a grid:
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam eleifend elit sed est egestas, a sollicitudin mauris
tincidunt. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam eleifend elit sed est egestas, a sollicitudin mauris
tincidunt. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam eleifend elit sed est egestas, a sollicitudin mauris
tincidunt.Card
Card
Card
Copy to Clipboard
Copied!
Toggle line wrap
<rh-surface id="band" color-palette="light" class="theme-bordeaux">
<div class="band-grid">
<rh-card>
<h2 slot="header">Card</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam eleifend elit sed est egestas, a sollicitudin mauris
tincidunt.</p>
<rh-cta slot="footer" priority="primary" href="#">
Call to action
</rh-cta>
</rh-card>
<rh-card>
<h2 slot="header">Card</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam eleifend elit sed est egestas, a sollicitudin mauris
tincidunt.</p>
<rh-cta slot="footer" priority="primary" href="#">
Call to action
</rh-cta>
</rh-card>
<rh-card>
<h2 slot="header">Card</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam eleifend elit sed est egestas, a sollicitudin mauris
tincidunt.</p>
<rh-cta slot="footer" priority="primary" href="#">
Call to action
</rh-cta>
</rh-card>
</div>
</rh-surface>
Copy to Clipboard
Copied!
Toggle line wrap
rh-surface#band {
container-type: inline-size;
padding: var(--rh-space-4xl);
&.theme-bordeaux {
--bordeaux-darkest: #19050a;
--bordeaux-darker: #260710;
--bordeaux-dark: #330915;
--bordeaux-dark-alt: #290711;
--bordeaux-brand-dark: #7f1734;
--bordeaux-brand-light: #d52757;
--bordeaux-light: #a55d71;
--bordeaux-lighter: #d9b9c2;
--bordeaux-lightest: #f2e8eb;
--rh-color-surface-darkest: var(--bordeaux-darkest);
--rh-color-surface-darker: var(--bordeaux-darker);
--rh-color-surface-dark: var(--bordeaux-dark);
--rh-color-surface-dark-alt: var(--bordeaux-dark-alt);
--rh-color-surface-light: var(--bordeaux-light);
--rh-color-surface-lighter: var(--bordeaux-lighter);
--rh-color-surface-lightest: var(--bordeaux-lightest);
--rh-color-border-interactive-on-dark: var(--bordeaux-lightest);
--rh-color-border-interactive-on-light: var(--bordeaux-darkest);
--rh-color-interactive-primary-default-on-dark: var(--bordeaux-lighter);
--rh-color-interactive-primary-default-on-light: var(--bordeaux-darker);
--rh-color-interactive-primary-hover-on-dark: var(--bordeaux-light);
--rh-color-interactive-primary-hover-on-light: var(--bordeaux-dark);
--rh-color-interactive-primary-focus-on-dark: var(--bordeaux-light);
--rh-color-interactive-primary-focus-on-light: var(--bordeaux-dark);
--rh-color-interactive-primary-active-on-dark: var(--bordeaux-light);
--rh-color-interactive-primary-active-on-light: var(--bordeaux-dark);
--rh-color-border-subtle-on-dark: var(--bordeaux-lighter);
--rh-color-border-subtle-on-light: var(--bordeaux-darker);
--rh-color-icon-primary-on-light: var(--bordeaux-brand-dark);
--rh-color-icon-primary-on-dark: var(--bordeaux-brand-light);
}
}
.band-grid {
display: grid;
grid-template-columns: 1fr;
max-width: 992px;
margin: 0 auto;
gap: var(--rh-space-2xl);
@container (min-width: 567px) {
grid-template-columns: repeat(3, 1fr);
}
}
Copy to Clipboard
Copied!
Toggle line wrap
The color-palette
attribute
The color-palette
attribute is a fundamental aspect of the theming system. The
attribute is available on specially designated provider elements that actively
define a color palette, while their children passively accept their
background color and text color. The color-palette
can be set to six possible
values:
lightest
lighter
light
dark
darker
darkest
Theming whole pages
To theme an entire page, you may replace the <main>
element with an
<rh-surface role="main">
. This ensures that computed theme tokens propagate to
all children while maintaining the proper landmark semantics.
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>My Themed Page</title>
</head>
<body>
<rh-surface role="main" color-palette="darker">
...
</rh-surface>
</body>
</html>
Theming individual sections
You may also theme particular sections, giving them a contrasting color palette. Read more about the design considerations for contrasting sections on the color-palettes page.
<body>
<rh-surface role="main">
<section aria-labelledby="heading">
<h2 id="default">Default palette</h2>
<p>Default color palette (i.e. lightest)</p>
</section>
<rh-surface role="section"
aria-labelledby="darker"
color-palette="darker">
<h2 id="darker">Darker palette</h2>
<p>Contrasting color palette (i.e. darker)</p>
</rh-surface>
</rh-surface>
</body>
Writing themes
When theming pages, sections, or elements, developers should carefully review the designs they received and seek to make as few customizations as necessary. The ideal order of operations is as follows:
- Search for an appropriate or approximate pattern
- Use element-specific tokens to customize an element e.g.
--rh-card-header-background-on-light
- Use theme tokens to customize the target e.g.
--rh-color-interactive-primary-default-on-light
- Use element CSS Shadow Parts for greater control
_
). These should be considered "private" and may change at any time without warning.
Art Direction
Art direction is the process of selecting art assets based on the context in which they are viewed. Regarding theming, art direction means choosing or modifying graphics based on the surrounding theme or color palette. There are two ways to approach this:
- Dynamic graphics
- Alternating graphics
Dynamic graphics
Page authors can create dynamic graphics that respond to their surrounding
theme by using inline SVGs that reference theme tokens. For example, this SVG
graphic uses the --rh-color-border-interactive
theme token to style a
rectangle.
<svg slot="header" width="80" height="80">
<rect fill="var(--rh-color-border-interactive)"
fill-opacity="0.1"
stroke-dasharray="4"
stroke-width="1"
stroke="var(--rh-color-border-interactive)"
width="80"
height="80"/>
</svg>
This approach does not work with SVGs loaded through the <img>
tag, or with
raster graphics; however, another approach is in development that could help.
Alternating Graphics
Developers can accomplish graphical art direction in their themed pages by swapping linked or raster graphics depending on the context. For example, if the Red Hat logo appears on sections with both light and dark backgrounds, developers should carefully choose the graphic that matches the background.
<rh-surface role="section"
aria-labelledby="products"
color-palette="darkest">
<h2 id="products">Products</h2>
<img alt="Red Hat Enterprise Linux"
src="/assets/logos/products/rhel-on-dark.svg">
</rh-surface>
<rh-picture>
Planned
We’re now in the early stages of developing an art direction element for these
scenarios, tentatively named <rh-picture>
. The following speculative code
block shows how the element may be used in the future to enable responsive
themable graphics.
<rh-surface role="section"
aria-labelledby="products"
color-palette="darkest">
<h2 id="products">Products</h2>
<rh-picture>
<source srcset="/assets/logos/products/rhel-on-dark.png" color-theme="dark"></source>
<source srcset="/assets/logos/products/rhel-on-light.png" color-theme="light"></source>
<img src="/assets/logos/products/rhel.png" alt="Red Hat Enterprise Linux">
<rh-picture>
</rh-surface>
How theming works
We've developed our theming system with web standards in mind, aiming to use CSS over JavaScript as much as possible. At the current state of browser technology, we found that some JavaScript is required for the system to work correctly. But we soon expect to be able to remove all (or nearly all) of that JavaScript and have the theming system work entirely (or almost entirely) through plain CSS. The following explains how the system currently operates, how element authors can hook into it, and what the future holds for theming.
Current implementation - Context Protocol
RHDS’s theming system is primarily about styles. It currently relies on JavaScript to work, via the context protocol developed by the web components community to support passing data between components.
Our system utilizes this protocol with the setting of the color-palette
attribute on a provider element, which makes its context data (in our case,
light
or dark
) available to its children. By doing so we ensure accessible
colors are applied given any possible change in context value higher up in the
DOM tree.
This is important not only for helping us maintain our design guidelines but also for accessibility compliance and enabling great experiences for all our users.
The context protocol is enabled by two reactive controllers: the provider controller and the consumer controller. The provider and consumer controllers work together. The provider supplies the context data, while the consumer receives and uses it within child elements. Custom elements can implement the consumer, the provider, or both, depending on the needs of that particular element.
Providers
Custom Elements that implement the provider controller are elements that provide
their own context, overriding that of their parent. Elements such as
<rh-surface>
, <rh-card>
, and <rh-accordion>
are examples of such context
providers. If a provider contains a set color-palette
attribute, it will
override any parent context and pass its context on to its children.
To make your element a color context provider:
- First import the provider controller.
- Then add the
@colorContextProvider()
decorator to a property with the attributecolor-palette
which is the typeColorPalette
.
import { LitElement } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
import {
colorContextProvider, // 1
type ColorPalette,
} from '@rhds/elements/lib/context/color/provider.js';
@customElement('rh-provider')
export class RhProvider extends LitElement {
@colorContextProvider() // 2
@property({ reflect: true, attribute: 'color-palette' })
colorPalette?: ColorPalette;
}
Read “What are color palettes” for more information about the six
available color-palettes
.
Consumers
Context consumers are elements that passively consume the parent context.
Elements such as <rh-cta>
, <rh-badge>
and <rh-tag>
are examples of
consumers.
To make your element a color context consumer:
- First
import
the classMap Lit directive, and the consumer controller. - Then add the
@colorContextConsumer()
decorator to a private property ofon
which is the type ofColorTheme
. - Add a classMap that implements the shadow class in your render method.
- Use the theming tokens in your element’s shadow styles, being sure
to select the element that has the
.on.light
/.on.dark
classes.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { classMap } from 'lit/directives/class-map.js'; // 1
import {
colorContextConsumer,
type ColorTheme,
} from '@rhds/elements/lib/context/color/consumer.js';
@customElement('rh-consumer')
export class RhConsumer extends LitElement {
@colorContextConsumer() private accessor on: ColorTheme | undefined; // 2
render() {
const { on = 'light' } = this; // 3
return html`
<div id="container"
class="${classMap({ on: true, [on]: true })}">
</div>`;
}
}
#container {
color: var(--rh-color-text-primary);
background: var(--rh-color-surface);
}
What the @colorContextConsumer
decorator does, in addition to participating in
the context event system, is apply a stylesheet from
@rhds/tokens/css/color-context-consumer.css.js
to the element’s shadow root.
That stylesheet selects well-known class names .on.light
and .on.dark
,
and applies values to the theming tokens, depending on the context received.
.on.light {
--rh-color-text-primary: var(--rh-color-text-primary-on-light, #151515);
/* etc... */
}
.on.dark {
--rh-color-text-primary: var(--rh-color-text-primary-on-dark, #ffffff);
/* etc... */
}
For more information on the significance of the context values (i.e.
ColorTheme
), read “Background”.
Future state - style queries
Planned
In the not-so-distant future, we will be able to replace the context protocol completely and remove this code by implementing the web standard container style queries.
In anticipation of this upcoming browser feature, we attempt to ensure that our theming system as implemented today using context can easily be replaced with style queries in the near future.
In addition to reducing the JavaScript payload of the design system, style queries will allow authors to attach surface styles to any element. For example, your page markup declares a custom surface with some classes:
<div class="custom-surface dark">
<rh-cta href="#">GO!</rh-cta>
</div>
And your document CSS sets the desired color context:
.custom-surface {
container: surface;
&.dark { --rh-background-context: dark; }
&.light { --rh-background-context: light; }
}
And you would declare the import a shared stylesheet that activates the theming system,
similarly to how elements import @rhds/tokens/css/color-context-consumer.css.js
.
@container style (--rh-context-background: dark) {
--rh-color-text-primary: var(--rh-color-text-primary-on-dark)
}