Luke UI
GitHub repository

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.

Colour — Roles and modes
Light mode
Account statusWarning
This notice uses the selected semantic role.
Dark mode
Account statusWarning
This notice uses the selected semantic role.

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.

Continue learning