On this page

Foundations

Composition and customisation

Choose a Luke UI component, primitive, or token for custom interface patterns.

Start with a semantic component when its purpose matches the task. It handles the intended behaviour, accessibility, and visual treatment together. Compose from a documented primitive only when a component's props do not fit.

Start with the component API

Button includes its standard label, icon slots, and pending treatment. Use the button primitive when you need a different child layout or loading treatment while keeping Luke UI button behaviour and styling.

Each primitive page documents the structure and accessibility responsibilities for that primitive: the label, description, state, and control relationships it expects. Preserve or author those relationships when you compose one. Do not depend on React Aria contexts, slots, or Luke UI implementation details as an application API.

import { Button } from '@luke-ui/react/primitives/button';

export function SaveShortcutButton() {
	return (
		<Button prominence="high">
			<span>Save changes</span>
			<span aria-hidden>⌘S</span>
		</Button>
	);
}

Build a custom composition

Choose the primitive that owns the behaviour you need. Then compose its documented public parts. Start from the control's semantic root, such as TextInputRoot or ComboboxRoot. The root connects the label, description, and error inside it to the control. Inside the root, use Field for the standard label, description, and error arrangement. This example adds a prefix with the TextInput primitives. Use the combobox primitives when ComboboxField does not fit your control, popover, or loading UI.

Composition and customisation: Build a custom composition
Enter an amount in dollars.
import { Field } from '@luke-ui/react/primitives/field';
import {
	TextInput,
	TextInputControl,
	TextInputPrefix,
	TextInputRoot,
} from '@luke-ui/react/primitives/text-input';

export default () => {
	return (
		<TextInputRoot name="amount">
			<Field description="Enter an amount in dollars." label="Amount">
				<TextInputControl>
					<TextInputPrefix>$</TextInputPrefix>
					<TextInput inputMode="decimal" placeholder="0.00" />
				</TextInputControl>
			</Field>
		</TextInputRoot>
	);
};

Use primitives when your layout owns a control's visible label, such as a settings row with the label beside a switch. Place FieldLabel and FieldDescription inside the control's root. The root names and describes the control from them, with no ids to pass. The Switch primitive shows this layout.

Give the custom component the same accessibility care as a normal component. Preserve its documented structure. Provide an accessible name. Keep keyboard and focus behaviour intact. Use a normal React element for surrounding content when no primitive is needed.

Style from interaction state

Interactive primitives keep fixed semantics. Prefer state-aware className and style over root replacement when you only need visual feedback. This example scales the button from press and hover state without replacing the <button>:

Composition and customisation: Style from interaction state
import { Button } from '@luke-ui/react/primitives/button';

export default () => {
	return (
		<Button
			style={({ isHovered, isPressed }) => {
				const scale: number = (() => {
					if (isPressed) return 0.98;
					if (isHovered) return 1.02;
					return 1;
				})();

				return {
					transform: `scale(${scale})`,
					transition: 'transform 100ms',
				};
			}}
		>
			Continue
		</Button>
	);
};

Do not turn a button into a link. Use Link or IconLink, or style an owned element with buttonRecipe.

Own a layout or typography root

elementType and renderRoot are opt-in. Layout primitives such as Box, and typography such as Text and Heading, expose them when choosing or owning the root is a real composition need. Use elementType for a supported semantic or structural element. Use renderRoot when a callback must own the element and its DOM attributes. Do not combine them. Interactive controls generally omit both.

Read the Box documentation for the supported elements and the renderRoot callback's resolved props.

Style the new pattern

Use public semantic variables from @luke-ui/react/theme for custom surfaces, typography, colour, radius, and shadow. They follow the active theme and colour mode. Read Styling for when to use vars, and the token reference for the contract.

Read the primitive documentation before you build a custom component from a specific primitive.

Continue learning