Theming
The default styles read every color, size and duration
from a CSS custom property whose name starts with uni-, so they never meet
your own. A theme of yours is a set of values for those properties: from a
palette the package generates, or from your own design tokens.
Every color names a theme and a role in it: --uni-danger-border-color is
the danger theme’s border. The default theme leaves its name out:
--uni-border-color. Every theme has every role, so a field that turns
invalid swaps border for danger-border, and no name needs looking up. In
Tasty the same colors are tokens: #uni-danger-border, #uni-border.
| Theme | Colors | Used by |
|---|---|---|
default |
Neutral, the base of every component: surfaces, text, borders. | All 17. |
accent |
Selection, primary actions and the focus ring. | Checkbox, Switch, Radio, ToggleButton, options, Button. |
danger |
Invalid fields, error messages, destructive actions. | Every field’s invalid state and error, data-variant="danger". |
warning |
Cautions. | No component yet: they are there for yours. |
note |
Information to point out. | As warning. |
disabled |
A disabled component, which takes it whole. | Every component that can be disabled. |
| Role | Colors | Contrast, in every scheme |
|---|---|---|
surface |
The theme’s background: controls and panels in the default theme, a tinted one in the others. | None of its own. |
surface-hover, surface-pressed |
That background, hovered and pressed. | None of their own. |
text |
Text: body text, an error message. | 4.5:1 on its own surface and on surface. |
text-soft |
Secondary text: descriptions, placeholders. | As text. |
border |
Edges: a field’s box, a checkbox. | 3:1 against surface. |
fill |
The theme’s strong color: a selected indicator, a primary button; in the default theme, a tooltip. | None of its own: the same in every scheme. |
fill-hover, fill-pressed |
That fill, hovered and pressed: a step darker each. | As fill. |
fill-text |
Text and marks on the fill: a check mark, a primary button’s label. | 4.5:1 on its fill. |
outline |
The focus ring. | 3:1 against its surface and surface. |
The default theme’s fill, a tooltip’s, also stands off the page, at 1.3:1 at
least. The default theme also has --uni-shadow-color, for popups and
dialogs, and --uni-backdrop-color, behind a dialog: both are shadow colors,
dark and translucent, with an alpha fitted to the page in each scheme, so the
backdrop dims a dark page as well as a light one. The disabled theme keeps 3:1
for its text and nothing more, since WCAG exempts disabled controls.
View source
<!doctype html><meta charset="utf-8"><style>.schemes {
display: grid;
gap: 1rem;
}
section {
overflow-x: auto;
padding: 0.75rem;
border-radius: var(--uni-radius);
background: var(--uni-surface-color);
color: var(--uni-text-color);
font:
0.75rem / 1.125rem system-ui,
sans-serif;
}
h3 {
margin: 0 0 0.5rem;
font-size: 0.875rem;
}
th {
padding-inline-end: 0.5rem;
font-weight: 500;
text-align: start;
white-space: nowrap;
}
td::before {
content: '';
display: block;
inline-size: 2rem;
block-size: 1.25rem;
border-radius: 0.25rem;
background: var(--swatch);
box-shadow: inset 0 0 0 1px var(--uni-border-color);
}
</style><div class="schemes">
<section data-uni-scheme="light">
<h3>Light</h3>
<table data-swatches></table>
</section>
<section data-uni-scheme="dark">
<h3>Dark</h3>
<table data-swatches></table>
</section>
</div>
<script src="/demo/theming.js"></script>The palette has a light, a dark and a high-contrast scheme. The page follows
the OS: dark under prefers-color-scheme: dark, and high contrast under
prefers-contrast: more. To force one, set data-uni-scheme to light or
dark on <html> for the page, or on any element for what is inside it:
<aside data-uni-scheme="dark">…</aside>High contrast follows the OS in a forced scheme too. Each scheme sets
color-scheme, so scrollbars and native controls match it. In forced colors
mode, the components draw their fills, borders and marks in the system’s
colors.
The palette comes from Glaze: a hue and a
saturation for each theme, from which it derives every role in every scheme,
at the contrast above. createPalette() takes the seeds you change, and keeps
the package’s for the rest:
import { createPalette } from '@uniaria/styles/palette';
const palette = createPalette({
default: { hue: 200, saturation: 10 },
accent: { hue: 150 },
});| Theme | Hue | Saturation |
|---|---|---|
default |
262 | 20 |
accent |
262 | 100 |
danger |
25 | 100 |
warning |
75 | 100 |
note |
200 | 100 |
disabled |
262 | 20 |
createPalette() needs @tenphi/glaze, and runs in Node and in the browser.
Pass its result to buildCss(), to the
Tasty plugin, or to paletteCss(), which writes it as
CSS while the page runs (below).
A design system with its own color tokens leaves the palette out and points the roles at its tokens. The contrast in the tables above is then yours to keep.
-
With the prebuilt CSS, import
components.csswithouttokens.css, and set the properties:@import '@uniaria/styles/css/components.css'; :root { --uni-accent-fill-color: var(--brand-600); /* …and the other roles. */ } -
With
buildCss(), the same properties as tokens, which go on:rootwithout a palette:buildCss({ palette: false, tokens: { '#uni-accent-fill': 'var(--brand-600)' }, }); -
In Tasty,
uniaria({ palette: false }), and the roles set as tokens, as your own are: in global styles such as yourbody’s, where a state map can switch them with your schemes, in the plugin’stokensoption, which writes them on:root, or in an element’s styles, for what is inside it:configure({ plugins: [uniaria({ palette: false })], globalStyles: { body: { '#uni-surface': '#surface', '#uni-accent-fill': '#brand-600', // …and the other roles. }, }, });Not with
replaceTokens: it writes your token into each style in place of the role, and leaves no--uni-*property to change while the page runs.
A role that points at a token of yours takes that token’s value where the role is set, so set the roles where your tokens change with the scheme, or below.
A single token changes with buildCss({ tokens }) or the plugin’s tokens
option. A color takes one value for every scheme, or one per scheme (light,
dark, lightContrast, darkContrast), and the others keep the palette’s:
buildCss({
tokens: {
'$uni-radius': '4px',
'#uni-accent-outline': { light: '#0a7', dark: '#3c9' },
},
});The sizes and durations:
| Token | Default | Use |
|---|---|---|
--uni-gap |
0.5rem |
The gap unit: padding and spacing. |
--uni-radius |
0.375rem |
Corners. |
--uni-border-width, --uni-outline-width |
1px, 2px |
Borders, and the focus ring. |
--uni-control-height |
2rem |
The least height of buttons, inputs and triggers. |
--uni-font-size, --uni-line-height |
0.875rem, 1.25rem |
Text. |
--uni-font-size-small, --uni-line-height-small |
0.75rem, 1.125rem |
Descriptions, errors and tooltips. |
--uni-transition |
80ms |
Hover and press, and the popups of Select, Combobox and Tooltip. |
--uni-overlay-transition |
120ms |
A popover opening and closing. |
--uni-dialog-transition |
250ms |
A dialog and its backdrop opening and closing. |
--uni-z-overlay |
1000 |
Popovers in browsers without the Popover API. |
--uni-quiet-hover, --uni-quiet-pressed |
.04, .06 (light); .031, .046 (dark) |
How much of its text color a quiet button mixes into its fill, hovered and pressed. |
Controls grow with the text: the sizes are in rem, and the control height is
a minimum. Under prefers-reduced-motion: reduce, nothing transitions. The
quiet steps differ by scheme, so they are written with the palette, in each
scheme’s block; give them one value or one per scheme, as a color.
Whichever output you use:
-
A scheme: set or remove
data-uni-scheme. -
A token: set its property on any element, for what is inside it:
element.style.setProperty('--uni-accent-fill-color', color). A forced scheme inside it keeps its own. -
The palette, chosen on the page, as from a color picker:
paletteCss()writes the same rules astokens.css, to add after the default ones.import { createPalette, paletteCss } from '@uniaria/styles/palette'; const sheet = new CSSStyleSheet(); sheet.replaceSync(paletteCss(createPalette({ accent: { hue } }))); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
The plugin takes the palette and the tokens. Configure it once, before anything renders:
import { configure } from '@tenphi/tasty';
import { createPalette } from '@uniaria/styles/palette';
import { uniaria } from '@uniaria/styles/tasty';
configure({
plugins: [
uniaria({
palette: createPalette({ accent: { hue: 300 } }),
tokens: { '#uni-accent-outline': { light: '#0a7', dark: '#3c9' } },
}),
],
// Yours: the plugin sets no units.
units: { x: '8px', r: '6px' },
});The plugin writes the tokens as global styles, one block per scheme, so
data-uni-scheme works as with the prebuilt CSS. Set the package’s tokens
through its options rather than through configure({ tokens }): Tasty
renders those on :root only, where the scheme blocks override them. Without
a palette, the plugin takes the package’s, which ships as data, so it needs
no Glaze.