Number Field
One glass group holding a numeric input and two flattened steppers.
Installation
npx shadcn@latest add https://liqui.design/r/number-field.jsonInstalling more than one? Register the namespace once in components.json and
drop the URLs:
{
"registries": {
"@liqui-design": "https://liqui.design/r/{name}.json"
}
}npx shadcn@latest add @liqui-design/number-fieldUsage
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldLabel,
} from '@/components/ui/number-field';const id = React.useId();
<NumberField id={id} defaultValue={24} min={0} max={200} step={2}>
<NumberFieldLabel htmlFor={id}>Bezel width</NumberFieldLabel>
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>;The id goes on the root and the label points back at it. Base UI puts that id
on the input, so this is the one place the wiring is manual.
Formatting
format takes Intl.NumberFormatOptions straight through, so a percentage, a
currency or a unit is a prop rather than a wrapper:
<NumberField format={{ style: 'percent', maximumFractionDigits: 0 }} step={0.05} />The value stays a number — 0.35 above — and only its presentation changes.
Notes
One surface, three controls
The group is the glass and everything in it is flat. A control inside a glass box has that box's tint behind it rather than the page, so a lens there bends the surface it is lying on: three lenses in a 180px-wide box would be three smudges. Same rule as Dialog's dismiss and Toggle Group's toggles.
The steppers are drawn as divisions of the group rather than as buttons — a hairline, a hover wash, nothing else:
'shadow-[inset_-1px_0_0_color-mix(in_srgb,var(--lq-text)_14%,transparent)]';A raised key would need its own cast shadow to look raised, and a shadow laid on top of glass is the one thing the material cannot sit under: it darkens the refraction instead of falling across it.
The input is inside the glass, not the glass
The <input> is a transparent element in the content layer.
Field explains why at length: an <input> is a
replaced element, and the backdrop, tint and specular layers cannot render
inside one. The focus ring lives on the group and is driven by has-*, so the
surface reacts to a state that belongs to its child:
'has-[input:focus-visible]:shadow-[0_0_0_3px_color-mix(in_srgb,var(--lq-accent)_35%,transparent)]';The label is a scrub handle
Drag NumberFieldLabel left or right and the value follows. Base UI hides the
pointer for the duration and draws the arrow cursor in its place, which is why
the label is a component here instead of a <label> you bring — the gesture
needs an element Base UI controls.
It is the same interaction the slider offers with a thumb, minus the surface. Worth knowing it is there: on a dense form it is faster than either the steppers or the keyboard, and there is nothing on screen advertising it.
Sizing
The group is w-45 and h-9.5 with radius: 12, which is
Field's control at the same optics. If you make it
taller, move radius with it; if you make it much shorter, bring bezel down
too, or the two steppers end up entirely inside the refracting rim.