# Colour (/docs/color)



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 [#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.

```tsx
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 [#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.

apps/docs/src/examples/overview/role-colours.tsx

```tsx
import { Box } from '@luke-ui/react/box';
import { Button } from '@luke-ui/react/button';
import { Text } from '@luke-ui/react/text';
import { vars } from '@luke-ui/react/theme';
import { useState } from 'react';

const roles = {
	neutral: 'Neutral',
	accent: 'Accent',
	info: 'Info',
	success: 'Success',
	warning: 'Warning',
	danger: 'Danger',
} as const;

type Role = keyof typeof roles;

const modes = ['light', 'dark'] as const;

export default () => {
	const [role, setRole] = useState<Role>('warning');

	return (
		<Box display="grid" gap="sp16">
			<Box aria-label="Semantic role" display="flex" flexWrap="wrap" gap="sp8" role="group">
				{(Object.keys(roles) as Array<Role>).map((option) => (
					<Button
						appearance={role === option ? 'solid' : 'subtle'}
						aria-pressed={role === option}
						key={option}
						onPress={() => setRole(option)}
					>
						{roles[option]}
					</Button>
				))}
			</Box>
			<Box
				display="grid"
				gap="sp12"
				style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(min(16rem, 100%), 1fr))' }}
			>
				{modes.map((mode) => (
					<Box
						data-color-mode={mode}
						display="grid"
						gap="sp8"
						key={mode}
						padding="sp12"
						style={{
							backgroundColor: vars.color.surface.canvas,
							borderRadius: vars.radius.surface,
						}}
					>
						<Text elementType="strong" fontWeight="emphasis" typography="caption">
							{mode === 'light' ? 'Light mode' : 'Dark mode'}
						</Text>
						<Box
							display="grid"
							gap="sp8"
							padding="sp8"
							style={{
								backgroundColor: vars.color.surface.floating,
								border: `1px solid ${vars.color.border.decorative}`,
								borderRadius: vars.radius.surface,
							}}
						>
							<Box
								display="grid"
								gap="sp4"
								padding="sp12"
								style={{
									backgroundColor: vars.color.background[role].subtle.rest,
									border: `1px solid ${vars.color.border[role]}`,
									borderRadius: vars.radius.control,
								}}
							>
								<Box alignItems="center" display="flex" flexWrap="wrap" gap="sp4">
									<Text
										elementType="strong"
										fontWeight="emphasis"
										style={{ color: vars.color.foreground[role].rest }}
									>
										Account status
									</Text>
									<Text
										elementType="span"
										fontWeight="emphasis"
										typography="caption"
										style={{
											backgroundColor: vars.color.background[role].solid.rest,
											borderRadius: vars.radius.full,
											color: vars.color.foreground[role].onSolid,
											padding: `${vars.space.sp4} ${vars.space.sp8}`,
										}}
									>
										{roles[role]}
									</Text>
								</Box>
								<Text style={{ color: vars.color.foreground[role].rest }}>
									This notice uses the selected semantic role.
								</Text>
							</Box>
						</Box>
					</Box>
				))}
			</Box>
		</Box>
	);
};
```

### Background and foreground [#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:

```tsx
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:

```tsx
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 [#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](/docs/authoring-a-theme#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 [#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 [#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 [#continue-learning]

<Cards>
  <Card href="/docs/applying-a-theme" title="Applying a theme">
    Follow the system preference or choose a fixed colour mode.
  </Card>

  <Card href="/docs/token-reference" title="Token reference">
    Browse the full public semantic-colour contract.
  </Card>
</Cards>
