Skip to main content

Kits

A kit is a named visual identity: a bundle of parts, tokens and presentation defaults that dresses every control beneath it.

Composing controls covers dressing one control by hand. A kit is the same thing applied to a whole panel — and it is how a look travels between projects, because a kit is a plain object and publishing one is publishing a module.

import { AmbientKitProvider, AmbientKnob, AmbientSwitch, consoleKit } from "@ambientcss/components";

<AmbientKitProvider kit={consoleKit}>
<AmbientKnob label="LEVEL" defaultValue={68} />
<AmbientSwitch label="ON" defaultValue />
</AmbientKitProvider>

The call sites do not change. Only the look does — the two columns below render the same AmbientKnob and AmbientSwitch with the same props off the same state, and differ only in the kit above them.

LEVEL
ON

grounded (default)

LEVEL
ON

consoleKit

Or build one without writing any of it: the Kit Builder gives each control a tab, the control itself a screen, and its parts a row of keys beneath it — open a key and you are editing that part. What it hands back is the module: a real ControlKit, its imports, and the scene it was drawn under. It goes all the way down to the parts themselves: see Composing a kit without code below.

What ships

  • groundedKit — the Blender-grounded hardware look. The default, used whenever no kit is set.
  • consoleKit — a mixer-desk identity: cuboid bar knobs seated in a circular groove, pill toggles that light up with the panel accent, and key banks whose selection reads as a depressed key rather than a lit one.

Writing one

The Kit Builder will write this file for you; what follows is what it writes.

import type { ControlKit } from "@ambientcss/components";

export const neon: ControlKit = {
name: "neon",
rotary: (look) => ({
className: "neon-knob",
parts: { base: <span className="neon-body" />, actuator: <span className="neon-pointer" /> }
}),
looks: { rotary: [] }
};

Three things and no more:

FieldWhat it does
<family>(look) => { parts, className } for rotary, travel, press, latch, bank
defaultspresentation-only prop defaults — travel, input, animate
lookswhich look keys each family honours, used to warn in development

A kit may be partial. consoleKit dresses knobs, switches and key banks and lets faders and buttons fall through to groundedKit. That is what a real third-party kit looks like: it is not obliged to have an opinion about everything.

A kit is paint, not behaviour. Everything a part needs is already published on the control root — --ambx-percent, --ambx-angle, --ambx-size, data-dragging — so most of a kit is a stylesheet and a handful of <span>s. The console toggle's track is one element that mixes its own colour from --ambx-percent; no React state reaches it and the mechanism does not know it exists.

className replaces, it does not join. The root class is part of the look: .amb-knob carries the grounded knob's own token table, and a kit that replaces the parts has no use for it.

Composing a kit without code

There is no shallower mode in the Kit Builder: what a tab shows you is always the real thing — the control's four frames as a list of elements, each one either a part this package exports or a shape, which is a <span> with a generated rule behind it. Every part in the row of keys under a control can be opened, including the two the library ships.

A shape is not a lesser way of writing a part; it is what a part is. ConsoleWell is a groove ring with a flat disc in it. ConsoleBar is one chamfered box that restates the light in its own turned coordinates. Both are reachable from the form:

FieldWhat it writes
Placementinset, a centred width/height, an edge anchor — including outside the control's box, which is what a panel graphic needs — or in flow, the one a button's cap has to use
Structure.ambient (a body that cuts edges and casts), .amb-groove (a recess), or neither. Never both: each paints the whole lighting model into box-shadow
Surface, edge, materialamb-surface*, amb-chamfer/amb-fillet, amb-mat-*
Thickness, elevation--amb-thickness, --amb-elevation, as fractions — the console housing is 0.27 deep
Colouran albedo the light then acts on, a flat paint, the scene's recess floor, the accent, lit by value (mixes the accent in from --ambx-percent, with no React anywhere near it), or ink
Keep the light stillemits the two-element light-capture trick the console bar uses

Start from seeds the list rather than locking it: grounded parts, console parts, or console, in shapes — the console knob, toggle and key rebuilt entirely out of shapes, with the reference's own numbers, so you can take one apart to see how it is made. A shipped look is a starting point in that builder, never a wall.

Two things live beside the parts rather than in them, because they belong to the control root: the root class — the library classes the kit keeps, which carry the token table a part expects and the margin a scale ring overhangs into — and the geometry a latch cannot express any other way, since a latch is its own track. Both are fields you can see and edit, not something inferred from what you picked.

A kit that uses shapes exports two files: the module, and the stylesheet it imports. Every generated rule is two classes wide, so it wins on specificity rather than on import order — which the app consuming the kit controls and the kit does not.

Look props are kit vocabulary

AmbientKnob takes knurling, markers and indicator. Those are the grounded kit's words — the console knob has no knurl; it has a bar, a housing and a centre mark.

Look props pass through, and a kit reads the ones it understands. This is what a CSS theme already does: a stylesheet that never implemented a class simply does not react to it. The common path stays fully typed, so a misspelt knurling is still a compile error, and the looks field makes the drop visible rather than silent:

[@ambientcss/components] the "console" kit's rotary does not use \knurling` — it is a look prop from another kit's vocabulary, so it will have no effect here.`

For another kit's vocabulary, pass look:

<AmbientKnob label="LEVEL" look={{ mark: false, legend: false }} />

Or use the preset the kit ships, which types its own words:

import { ConsoleKnob } from "@ambientcss/components";

<ConsoleKnob label="LEVEL" mark={false} legend={false} />

Precedence

Caller prop → kit default → mechanism default. A kit that falls through for a family has no say in that family's defaults either.

The console kit

All three families were measured off photographs of the real hardware.

GAIN
ARM

default

BARE

mark={false} legend={false}

Gain 40 · Bare 72 · safe · ch 1

The knob is a cuboid bar standing on a flat face, housed in a circular groove cut into the panel — the same construction the button uses, and the same numbers: the housing takes the button's seat depth, its clearance gap and its near-black floor. Inside that ring everything is the panel's own surface colour: the face has no body at all, and the bar is that same material again, so nothing there is told apart by tone. The dark ring, the bar's chamfered edges and the shadow it drops on the face are the whole drawing.

The bar is the pointer, and it runs the full width of the face, so a short dark mark is printed across it near one end to say which end reads. The accent centre mark and the −/+ legends sit outside the knob's own box, in the panel frame.

The bar rides the rotating actuator frame, which is a problem worth describing, because it is the kind a kit has to solve in CSS. A lit edge painted from the inherited light would turn with the bar and end up on the shadow side of the screen at half the angles. So the part rotates the light the other way instead: an outer span captures the scene's light vector, and the bar restates it in the frame's own turned coordinates —

--amb-light-x: calc(
var(--captured-x) * cos(var(--ambx-angle)) +
var(--captured-y) * sin(var(--ambx-angle))
);

— which keeps the bright edge and the drop shadow where the room put them while the bar sweeps under them. Two elements, because a custom property cannot read itself. Nothing about it reaches the mechanism: --ambx-angle is already published on the control root, so this is a stylesheet doing trigonometry.

Look props: mark (the centre mark, default on) and legend (the −/+ marks, default on). Its rotary defaults are input="angle" and a 280° sweep — the desk pot it is drawn from grabs where you press rather than tracking a drag.

The toggle is a pill track that fills with the accent as the switch travels, carrying a round accent thumb inside a white ring. Track 2.06:1, and a thumb at 0.9 of the track height — a thumb that nearly fills its groove, which is why the end clearance comes down to 0.05 with it. The thumb is flat-topped but not weightless: .ambient with a knob-scale thickness and no edge treatment at all, which is the combination that casts a drop shadow while leaving the top face unbroken.

Set the accent with --ambx-accent on the control or any ancestor; it defaults to the scene's own --amb-highlight-color, so a kit inherits the app's colour rather than imposing one.

<div style={{ "--ambx-accent": "#00a84d" }}>
<ConsoleToggle label="ON" defaultValue />
</div>
SEND
ON

#00a84d

SEND
ON

#f59e0b

The key is a squared cast face carrying the desk's own moulded circle — the tooling mark sunk into every key regardless of selection. Selection reads twice over: through the key's lamp, and through the face itself, which sits flush when lit. Both states are the same markup styled through [data-on] in .amb-console-bank: off stands at the desk's own knob-scale thickness — the body the bar and the toggle thumb stand at — and on drops to zero, reading as a button held all the way in. Because off and on are the same elements, the kit's bank needs neither onParts nor offParts; parts alone covers both.

The lamp itself has two readings, chosen by whether the key has a legend. A key without one carries a small LED at its centre that lights only when selected. A key with a legend has no LED — the text changes colour instead, from the panel's ink to the lamp colour, the engraved-and-filled legend on the referent's longer keys. Both read the same --amb-led-color channel, so one color prop — on the bank or per option — lights either alike:

import { AmbientSelect, consoleKit } from "@ambientcss/components";

// No legend: the selected key's LED lights.
<AmbientSelect
options={[{ value: "1", ariaLabel: "Channel 1" }, { value: "2", ariaLabel: "Channel 2" }]}
defaultValue="1"
/>

// With a legend: no LED — the text takes the lamp colour instead.
<AmbientSelect
color="#00a84d"
options={[
{ value: "a", label: "A", ariaLabel: "Send A" },
{ value: "b", label: "B", ariaLabel: "Send B" }
]}
defaultValue="a"
/>