Skip to main content

AmbientKnob

AmbientKnob is a rotary control that maps pointer movement to a numeric value. It is a preset: AmbientRotary supplies the kinematics, and the knob parts supply the look. Swap either.

Interactive preview

Drive

Value: 42

Grounded counterpart

The knob is modeled on its 3D referent (ambient3d/components/knob.py): a knob-scale body (thickness 2 — the referent's 9mm height) resting on the panel — a smooth chamfered cap, a knurled rim around it, and an indicator dot. The rim and the indicator rotate with the value, ribs and all; the cap does not, because its chamfer is lit from a fixed direction.

Both are machined the way a turned-and-knurled hardware knob is: the ribs stop short of the top, a bevel carries them out to the full radius, and a smooth chamfer and the flat cap sit above and inside it. knob.py takes that as knurl_rim / cap_chamfer, and the two rib sections are the same formula rather than two shapes fitted to each other — the clip path samples the referent's wall_r.

Live CSS
knob ground-truth render
Blender ground truth

Shape

The knob is described by three independent props rather than one variant name: whether the body is ribbed, what is printed on the panel around it, and what the face points with. They compose freely — every combination is a knob someone has built.

knurling

true (default) rings the cap with a 48-rib knurl: the cap sits back by the band's width and the ribs stand proud of it, clipped to a toothed annulus so they break the outline and rotate with the value. false gives a smooth turned body — the cap takes the full width and becomes the whole knob.

Both variants chamfer the cap's edge, the treatment knob.py cuts on every knob regardless of rib count. A rim of ribs is a grip, not an edge treatment: it says the knob turns, not where its top face stops.

material applies to both elements a knurled knob paints with, so material="shiny" is the machined-metal wheel with or without the ribs.

knurlColor

The grip ring's own colour — the one thing the ring may hold apart from the cap, because a dark grip round a pale cap is a real piece of hardware. Any CSS colour, read as an albedo rather than as paint: a dark knurl still takes the scene's exposure, the lamp's cast and the rim's own contact shading, and still goes dark when the lights do.

<AmbientKnob material="brushed-round" knurlColor="#1b1b1e" />

It is set on the ring itself, so it wins over the albedo a micro-relief material would otherwise put there — the finish keeps its grain and gives up only its tone. The cap has no matching prop: the cap's colour is the control's colour, set the ordinary way with --amb-albedo.

AmbientRotary still has no material prop and this preset does: how many elements a material has to reach is a fact about this knob's construction, and a mechanism cannot know it once the parts are yours.

Knurled
Smooth
Shiny
Live CSS
knob-smooth ground-truth render
Blender ground truth

markers

Printed scale dots on the panel around the knob, on the same arc the value sweeps — so a dot always sits where its value points, at any travel. They do not rotate, because they are panel graphics rather than part of the control, and they take --amb-label (the legend ink) rather than the accent colour.

  • none (default) — bare panel.
  • ends — the two dots the travel starts and stops at. They sit at 0.94 of the radius, so only their outer edge clears the knob's own footprint and no clearance is reserved.
  • full — 13 dots evenly spaced across the sweep. The ring reaches past the knob, so the component reserves that clearance — on all four sides, not just the three the arc needs, so the knob stays concentric with the box it occupies rather than shifting down off the point you positioned it on. The label clears the ring too, so it sits further from a knob with a full ring than from a bare one.
None
Ends
Full
Live CSS
knob-markers ground-truth render
Blender ground truth

indicator

  • circle (default) — the grounded referent's offset dot.
  • rectangle — a short radial bar out near the rim, running 0.50 to 0.84 of the radius.
Circle
Rectangle
Live CSS
knob-line ground-truth render
Blender ground truth

Props

PropTypeDefault
valuenumber-
minnumber0
maxnumber100
stepnumber1
labelstring-
knurlingbooleantrue
knurlColorstring (any CSS colour)-
markers"none" | "ends" | "full""none"
indicator"rectangle" | "circle""circle"
material"matte" | "shiny" | "glass" | "brushed" | "brushed-round" | "blasted"-
size"sm" | "md" | "lg""md"
travelnumber | { start, sweep }270
input"drag" | "angle" | "delta""drag"
animate"auto" | "follow" | "ease" | "snap""auto"
detentsnumberfrom step
dragDistancenumber200
wrapbooleanfalse
defaultValuenumbermin
onChange(nextValue: number) => void-

Also accepts standard HTMLAttributes<HTMLDivElement> (except native onChange).

Travel and pointer mapping

travel

How far the knob turns, in degrees clockwise from 12 o'clock. A bare number is a sweep centred on 12 o'clock, so 270 (the default) is the usual 8-to-4 pot and travel={240} is a narrower one. For an off-centre sweep, pass { start, sweep }.

Markers read the same sweep, so a dot always lands where its value points whatever you set here — ScaleRing takes the angles off the control rather than restating them.

input

Three genuinely different mappings, not one with a parameter:

  • drag (default) — pointer distance becomes a value delta. Drag up to increase, down to decrease; hold Shift for a quarter-speed fine mode. dragDistance is the pixels of travel for the full range. This works at any sweep and is the only mapping that behaves on touch, where a finger covers the knob it is turning.
  • angle — pointer position becomes an absolute angle, so the knob jumps to wherever you press. Needs a sweep under a full turn. Past the ends of the travel the value holds at whichever end is nearer.
  • delta — accumulated angular change, with no ends at all. This is the endless-encoder mapping; pair it with wrap to run past max and come round again.

At travel={360} or wider, angle is ambiguous — the first and last values share a screen position — so it is rejected with a warning and falls back to drag.

step, detents and animate

Three independent things that look like one:

  • step quantises the value. 0 leaves it continuous.
  • detents quantises the travel — how many rest positions the knob has. It defaults to the step grid, but an endless encoder can have 24 detents per turn with a continuous value.
  • animate says how the face moves to a new position: follow (1:1 with the pointer, no transition), ease, or snap. The default, auto, follows while you drag and eases otherwise.

Building your own

AmbientKnob is AmbientRotary plus a set of parts. Replace any of them — the parts are ordinary markup, and yours stand on exactly the same footing as the built-ins.

import { AmbientRotary, KnobBody, ScaleRing, useControlState } from "@ambientcss/components";

function Wedge() {
const { percent } = useControlState();
return <span className="my-wedge" style={{ opacity: 0.4 + percent * 0.6 }} />;
}

<AmbientRotary
value={gain}
onChange={setGain}
className="amb-knob"
label="Drive"
parts={{
panel: <ScaleRing count={7} />,
base: <KnobBody flush material="shiny" />,
actuator: <Wedge />,
}}
/>

Parts go into four frames, painted in this order:

FrameWhat it does
panelstatic, behind the control, allowed to overflow its box
basestatic, the control's own footprint
actuatorrotates to --ambx-angle — the moving part
fixturestatic, above the actuator

A part needs no state plumbing to react to the value: the control publishes --ambx-percent, --ambx-angle, --ambx-size and the rest on its own root, so a part can be pure CSS. useControlState() is the same data as JS, for parts that have to count something.

Want the body to stay still while a printed pointer sweeps? Put everything in base, leave actuator empty, and read --ambx-angle yourself.

Example

import { useState } from "react";

function GainKnob() {
const [gain, setGain] = useState(45);

return (
<AmbientKnob
label="Gain"
value={gain}
min={0}
max={100}
step={1}
onChange={setGain}
/>
);
}

Behavior notes

  • Value is clamped to [min, max] and snapped using step.
  • Arrow keys step by step, Page keys by ten of it, Home/End go to the ends.
  • Pass value for a controlled knob or defaultValue for an uncontrolled one; onChange fires either way.