Colour
Use semantic surfaces, content roles, and the shared role contract 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.surface.canvas: the page.color.surface.recessed: an inset area.color.surface.floating: elevated UI, such as menus and cards.color.surface.overlay: dialogs and other high-elevation surfaces. This is an opaque colour.
color.overlay.backdrop dims content behind a modal. It is not a surface colour.
Text uses primary and secondary roles. Borders distinguish decorative, control, and focus uses.
import { vars } from '@luke-ui/react/theme';
<section
style={{
backgroundColor: vars.color.surface.floating,
borderColor: vars.color.border.decorative,
color: vars.color.text.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. Every role offers the same capabilities, so a role's
meaning never limits what it can do:
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 in light and dark mode. The notice content stays constant, so only the role and mode change its colours.
Background and foreground
Background varies by the weight of the surface. Foreground varies by what the content sits on. This
is why a role has two background ramps, but one foreground ramp and a separate onSolid colour.
Each role has a subtle and a solid background ramp. Each ramp has rest, hover, and pressed
states:
import { vars } from '@luke-ui/react/theme';
vars.color.background.warning.subtle.rest;
vars.color.background.warning.subtle.hover;
vars.color.background.warning.subtle.pressed;
vars.color.background.warning.solid.rest;
vars.color.background.warning.solid.hover;
vars.color.background.warning.solid.pressed;Foreground gives each role rest, hover, and pressed colours, plus a colour guaranteed to read against 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 is a single token, not a state ramp. It must read against solid.rest, solid.hover, and
solid.pressed.
Built-in controls select these tokens for hover and pressed. Custom interactive elements should do the same.
Borders
Each role also has one state-free border, for example vars.color.border.warning. The token
describes meaning. The component decides when to apply it, such as only while invalid or as a
permanent outline.
Luke UI measures a semantic border for contrast, but does not guarantee it reaches 3:1. See contrast validation. Never rely on one alone to communicate a required state such as selected, invalid, or checked. Pair it with text, an icon, or another cue that is separately contrast-gated.
The numeric scale is private
Behind each role, the theme generator builds a numeric colour scale. This scale is an internal
implementation detail. It is never 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.
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 text.disabled role. The
public loadingSkeleton role is available when you build an equivalent custom element.