Theming
Colour
Use semantic surfaces, text, and roles instead of palette values.
Luke UI names colours by their job. A theme maps those roles to values for each identity and colour mode. A component then stays consistent, because it does not need to know the underlying palette.
Surfaces and content
Surface roles describe where an element sits in the interface:
color.background.surface.base: the default page and control background.color.background.surface.subdued: secondary regions, such as a sidebar or a panel set apart from the page.color.background.surface.raised: elevated panels, cards, menus, popovers, and dialogs. This is an opaque colour shared by inline and detached content.
color.background.backdrop dims content behind a modal. It is not a surface colour.
Choose borders and shadow separately. A raised surface does not add either, and flat cards can use
base with a border or subdued.
Text uses primary and secondary roles. Borders distinguish decorative, control, and role uses.
color.focus is a standalone colour, not a border, because it paints the focus outline. Text, role
foregrounds, color.border.control.{rest,hover,pressed}, color.border.<role>.strong, and
color.focus are guaranteed to contrast with all three surfaces. The control borders and
color.focus also clear every role's subtle background. color.border.decorative is a separator
with no contrast guarantee.
The focus ring paints color.focus, which clears 3:1 against the three surfaces and the six subtle
backgrounds. Inside a role's solid Box, such as Box backgroundColor="accent.solid", the ring
paints that role's onSolid colour instead, which clears 4.5:1 against the solid. Forced colours
use Highlight.
import { vars } from '@luke-ui/react/theme';
<section
style={{
backgroundColor: vars.color.background.surface.subdued,
borderColor: vars.color.border.decorative,
color: vars.color.foreground.primary,
}}
/>;Choose the role that describes the element's purpose. Do not copy a resolved colour into application CSS. It will not adapt when the identity or colour mode changes.
Semantic roles
Six roles cover the meanings a component needs beyond plain surfaces and text: neutral, accent,
info, success, warning, and danger. All six provide the same background, foreground, and
border tokens. Choose a role for what the colour communicates:
neutral: no accent or status meaning, distinct from ordinary surfaces.accent: preferred, selected, or emphasised. Not a promise of a brand colour.info: noteworthy factual context.success: completed, valid, or a positive result.warning: needs attention but has not failed and is not dangerous.danger: harm, failure, invalidity, or a critical condition.
Select a role to compare the same semantic token pairs. Change the document's colour mode to see the pairs in light and dark mode.
Background and foreground
Each role has a resting subtle background and a solid background with rest, hover, and
pressed states. Adaptive hover and pressed feedback uses the shared interaction tint pair:
import { vars } from '@luke-ui/react/theme';
vars.color.background.warning.subtle;
vars.interaction.tint.hover;
vars.interaction.tint.pressed;
vars.color.background.warning.solid.rest;
vars.color.background.warning.solid.hover;
vars.color.background.warning.solid.pressed;Use a role's rest, hover, and pressed foreground on any standard surface or any role's subtle
background. Use onSolid on that role's solid backgrounds:
vars.color.foreground.warning.rest;
vars.color.foreground.warning.hover;
vars.color.foreground.warning.pressed;
vars.color.foreground.warning.onSolid;onSolid reaches at least 4.5:1 against solid.rest, solid.hover, and solid.pressed. Each
state is visibly different from the one before it, whatever the theme's shadows and finishes.
Built-in controls select these tokens for hover and pressed. Custom interactive elements should do the same.
Borders
Each role has one state-free border, for example vars.color.border.warning.strong. The token
describes meaning. The component decides when to apply it, such as only while invalid or as a
permanent outline.
strong is the functional border. It reaches at least 3:1 against the three standard surfaces and
that role's own subtle background in both modes. It carries no guarantee against other backgrounds.
color.border.danger.strong marks an invalid control's boundary. Danger solid backgrounds carry the
on-solid text guarantee, but have no 3:1 guarantee against those surfaces.
Roles have no decorative border. Use color.border.decorative for a separator or other decorative
boundary. See contrast validation. Use
color.border.control.rest when a custom element needs a guaranteed boundary, and its hover and
pressed siblings for interaction feedback.
The numeric scale is private
Behind each role, the theme generator builds a numeric colour scale. It is not emitted as CSS and
has no public token. Use the semantic roles in your application, and use defineTheme with source
colours to author a theme.
Text on a background
A Luke UI background sets the text colour for everything inside it. A Box with backgroundColor
is one. So are the surfaces components paint, such as a Button fill, a field, a popover, and a code
chip. Each sets the text colour to its readable primary, which an Icon inside inherits, and tells
Text and Heading inside it which colours are readable:
| Background | primary and secondary | A role, such as color="danger" |
|---|---|---|
A surface, surface.* | foreground.primary and foreground.secondary | The role's foreground |
Any role's subtle fill | foreground.primary and foreground.secondary | The role's foreground |
A role's solid fill | That role's onSolid | That role's onSolid |
A role keeps its hue on another role's subtle fill. The theme compiler checks every pairing in the
table at 4.5:1 in both colour modes, and a theme that misses one does not compile. Pairings outside
the table are not checked, such as disabled text and text on a background from your own CSS. A
nested background sets a new context. A Box without backgroundColor keeps the surrounding one.
Box takes each role's resting subtle and solid fill. backgroundColor="accent.solid" paints
vars.color.background.accent.solid.rest, and Box values never carry a state suffix. Interactive
components own the hover and pressed fills and pair them for you. Use
vars.color.background.accent.solid.hover and .pressed in your own CSS.
Custom surfaces
To make a custom element a Luke UI background, set backgroundColor on a Box that also carries
your class. Put both on the same element rather than wrapping one in the other.
<Box backgroundColor="accent.solid" className={styles.card}>
<Heading>Example heading</Heading>
<Text>Inherits the fill's readable colour.</Text>
</Box>A background from your own CSS, such as vars.color.background.accent.solid.rest, sets no text
colour and does not change which colours Text reads. Nor does an image, a gradient, a translucent
colour, or CSS that overrides a Box background. Luke UI does not detect whether a background is
dark. Set the text colour in the same CSS and check its contrast yourself.
Disabled and loading states
Use the component's disabled or loading API when it has one. Those states account for interaction
and accessibility as well as colour. Disabled is a state, not a palette: interactive controls fade
with reduced opacity, and only field text recolours, through the public foreground.disabled role.
On a solid fill, disabled field text uses the fill's onSolid instead. The public
background.skeleton role is available when you build an equivalent custom element.