Light, dark and system
theme
belongs to the framework. It stores the preference and applies
data-theme
before the first paint.
UIThemeToggle
lets readers change it, also without JavaScript. Tokens change by themselves: do not write rules per theme.
The palette
Each scale has 12 steps: 1 and 2 for backgrounds, 3 to 5 for controls, 6 to 8 for borders, 9 and 10 for solids, 11 and 12 for text. The background is pure white in light and pure black in dark. Violet is for the main action, the focus, the selection and what is active: if everything is violet, nothing stands out.
--ui-color-primary-*
--ui-color-surface-*
--ui-color-info-*
--ui-color-success-*
--ui-color-warning-*
--ui-color-danger-*
Usage tokens
Read the usage token, not the step:
--ui-color-border, not
--ui-color-surface-6. No loose hex, size or font: if you miss something, it is added to UI. The whole contract is in
packages/ui/DESIGN.md.
.release-note {
border: var(--ui-border-width) solid var(--ui-color-border);
border-radius: var(--ui-radius-lg);
background: var(--ui-color-surface);
padding: var(--ui-space-4);
color: var(--ui-color-text-soft);
font-size: var(--ui-font-size-md);
}| Name | Type / default | Description |
|---|---|---|
--ui-color-bg / -bg-subtle
|
color
| Background of the page and quiet bands. |
--ui-color-surface / -surface-raised
|
color
| Card, panel and table; popover and modal. |
--ui-color-control / -hover / -active
|
color
| Background of a control and its states. |
--ui-color-text / -text-soft
|
color
| Text and secondary text. |
--ui-color-border / -border-strong / -control-border
|
color
| Separators, quiet borders and the border of a field at 3:1 or more. |
--ui-color-primary / -hover / -contrast
|
color
| Main action and the text on it. |
--ui-color-primary-soft / -text / -border
|
color
| A label or a quiet active background, with its text and its border. |
--ui-color-danger / -soft / -border / -text
|
color
| Info, success and warning have the same shape. |
--ui-font-size-xs … -6xl
|
rem
| Text sizes: reading at lg, controls at md, titles from 2xl up. |
--ui-space-* / --ui-radius-*
|
rem
| Space in steps of 0.25rem and radii from xs to xl. |
--ui-control-height-sm / -md / -lg
|
rem
| Heights of the controls, which grow by themselves with a touch pointer. |
--ui-duration-* / --ui-ease-*
|
time
| Motion: fast for color, base and slow for a panel. |
Fluid scale
The interface grows with the screen in one place, the global base: 16 px up to 1440, 18 px at 2560 and 21 px at 3840, always from the font size of the browser. Every token is in
rem, so they grow together. What is written in
px
does not grow.
ui: { base: false }
removes the base, and the scale with it.
Your brand
The sheets of
app/styles
load after the module: override the steps of a scale and everything that reads them changes with them.
/* app/styles/main.css: your brand, over the steps of each scale */
:root {
--ui-color-primary-9: #0a7cff;
--ui-color-primary-10: #0067d6;
--ui-radius-md: 0.5rem;
}
:root[data-theme='dark'] {
--ui-color-primary-9: #3b96ff;
--ui-color-primary-10: #66adff;
}
If your site already has its variables,
ui.tokens
accepts CSS values or references to them, and each key assigns its
--ui-*
tokens.
import { defineMeekoConfig } from '@meekojs/meeko/framework'
export default defineMeekoConfig({
modules: ['@meekojs/ui'],
theme: { default: 'dark' },
ui: {
// Each key assigns --ui-* tokens; a var() reference follows the themes of your site.
tokens: {
primary: 'var(--brand-action)',
primaryHover: 'var(--brand-action-hover)',
onPrimary: 'var(--brand-on-action)',
focus: 'var(--brand-focus)',
surface: 'var(--brand-surface)',
text: 'var(--brand-text)',
border: 'var(--brand-border)',
},
defaults: {
button: { shape: 'pill', size: 'md' },
link: { variant: 'outline' },
card: { variant: 'soft' },
},
},
})| Name | Type / default | Description |
|---|---|---|
primary / primaryHover / onPrimary
|
string
| --ui-color-primary, its hover and the text on it. |
accentSoft / accentText
|
string
| Backgrounds and text of active states: --ui-color-primary-soft and -text. |
surface / surfaceHover
|
string
| Surfaces and controls, and their states. |
text / textSoft / muted
|
string
| Main and secondary text. |
border / borderStrong
|
string
| Separators and outlines. |
focus / danger
|
string
| Keyboard focus and errors. |
font / radius
|
string
| Font family and base radius. |
controlHeight / controlPadding
|
string
| Dimensions of the actions. |
controlWeight / controlFontSize
|
string
| Weight and text size of the actions. |
Default values
ui.defaults
accepts
button,
link
and
card. The priority is: explicit props, application defaults and base values. Defaults are isolated per application and SSR request.
Texts and languages
The few texts components paint on their own (the close button of
UIModal, the empty state of
UITable
and
UICombobox, the names of
UIThemeToggle) come in Spanish and English. With
@meekojs/i18n
they follow the language of the document, without any configuration. Without that module,
ui.locale
chooses the language; Spanish by default.
ui.messages
overrides texts or adds other languages.
// meeko.config.ts
export default defineMeekoConfig({
modules: ['@meekojs/ui'],
ui: {
// Without @meekojs/i18n, the language of the texts; with it, the fallback of a language without texts.
locale: 'en',
messages: {
en: { table: { empty: 'Nothing here yet.' } },
fr: {
modal: { close: 'Fermer' },
table: { empty: 'Aucune donnée.' },
select: { empty: 'Aucun résultat', clear: 'Effacer la sélection' },
themeToggle: {
labels: { light: 'Clair', dark: 'Sombre', system: 'Système' },
unknown: 'Changer de thème',
group: 'Thème',
cycle: 'Thème : {current}. Passer à {next}',
},
},
},
},
})
A prop or a snippet of the component always win over these texts. What a language lacks comes from its base language (pt
for
pt-BR), from
ui.locale
and, last, from Spanish. The server and the client choose the same text, so hydration changes nothing.
The types
UIOptions,
UITokens
and
UIDefaults
are exported from
@meekojs/ui.