Skip to content
caliper ui docs

Components

Unit field

A number field with its unit beside it. Switching the unit converts the amount and the figure counts across to the new value rather than jumping, so it is clear the quantity is the same.

Free
pnpm dlx shadcn@latest add @caliper/unit-field

Shipment

Switch units; the amount stays the same.

kg
°C
Installs components/ui/unit-field.tsxDepends on none beyond ReactMotion honours reduced motion

When to use

Use it for

  • Weights, lengths, temperatures and distances people think of in different units.
  • Settings and forms used across regions.

Skip it when

  • Currencies; rates change, so use `MoneyField` with an explicit conversion.
  • Units that are not a straight line from one another, such as shoe sizes.

Decisions

Why it is built this way, so you know what to keep when you change your copy.

Behaviour

One base value
The amount is kept in a base unit and each unit is a straight line from it, so switching back and forth never drifts.
Counts across
The figure travels from the old number to the new over four tenths of a second, which says "same amount, new unit".

Usage

Unit in the name
The unit is read with the field, and the switch is a segmented control named after the field.

Installation

terminal
pnpm dlx shadcn@latest add @caliper/unit-field

Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage

This is the code behind the preview above. It is built from UnitField, toBase and fromBase.

example.tsx
import { type ReactNode } from "react"
import { UnitField, type FieldUnit } from "@/components/ui/unit-field"

function Panel({ title, context, className = "max-w-md", children }: { title: string; context: string; className?: string; children: ReactNode }) {
  return (
    <section className={`grid w-full gap-5 rounded-overlay border border-border bg-background p-6 shadow-resting max-sm:p-4 ${className}`}>
      <div className="grid gap-1">
        <p className="font-medium">{title}</p>
        <p className="text-sm text-muted-foreground">{context}</p>
      </div>
      {children}
    </section>
  );
}

const weights: FieldUnit[] = [
  { value: "kg", label: "kg", factor: 1, decimals: 1 },
  { value: "lb", label: "lb", factor: 0.45359237, decimals: 1 },
];

const temperatures: FieldUnit[] = [
  { value: "c", label: "°C", factor: 1, decimals: 1 },
  { value: "f", label: "°F", factor: 5 / 9, offset: -32 * (5 / 9), decimals: 0 },
];

export function UnitFieldDemo() {
  return (
    <Panel title="Shipment" context="Switch units; the amount stays the same.">
      <UnitField label="Parcel weight" units={weights} defaultValue={12.5} />
      <UnitField label="Keep below" units={temperatures} defaultValue={8} />
    </Panel>
  );
}

API reference

UnitField

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
label *string—
units *FieldUnit[]—
valuenumber—The amount in the base unit (the unit whose factor is 1).
defaultValuenumber0
onValueChange((base: number) => void)—
defaultUnitstring—

toBase

component

No props of its own; it accepts the native props of its root element.

fromBase

component

No props of its own; it accepts the native props of its root element.

Accessibility

It is built on native elements and keeps aria-describedby in step with state.

No custom key handling; Tab and the native activation keys do the work.

Motion

It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.

All Changing values examples