Components
Headless, composable numeric input field with drag-to-scrub, keyboard stepping, format/parse controls, and committed-value events for building any unit field.

"use client";
import * as React from "react";
import { UnitField } from "@/components/ui/unit-field";
const INPUT =
"w-full bg-transparent pr-2.5 text-right font-mono text-sm text-gray-700 tabular-nums outline-none placeholder:text-gray-300";
const DRAG_AREA =
"flex aspect-square w-9 shrink-0 cursor-ew-resize items-center justify-center text-[12px] font-medium text-gray-400 select-none";
const ROOT =
"relative flex h-9 w-full items-stretch border border-gray-200 transition-colors data-[focused=true]:border-gray-400";
function pxFormat(v: number) {
return `${v}px`;
}
function pxParse(v: string) {
const n = parseInt(v.replace("px", ""), 10);
return isNaN(n) ? null : n;
}
export default function UnitFieldPaddingDemo() {
const [top, setTop] = React.useState(16);
const [right, setRight] = React.useState(24);
const [bottom, setBottom] = React.useState(16);
const [left, setLeft] = React.useState(24);
return (
<div className="mx-auto w-full max-w-sm bg-white p-6 text-gray-700">
<header className="mb-3">
<div className="flex items-center gap-2">
<span className="font-mono text-[11px] text-gray-400">02</span>
<h3 className="text-[14px] font-medium text-black">Padding</h3>
</div>
<p className="mt-1 text-[13px] text-gray-500">
Grouped fields for spacing values.
</p>
</header>
<div className="grid grid-cols-2 gap-2">
<UnitField.Root
className={ROOT}
format={pxFormat}
parse={pxParse}
value={top}
onValueChange={setTop}
min={0}
max={100}
>
<UnitField.DragArea className={DRAG_AREA}>T</UnitField.DragArea>
<UnitField.Input className={INPUT} />
</UnitField.Root>
<UnitField.Root
className={ROOT}
format={pxFormat}
parse={pxParse}
value={right}
onValueChange={setRight}
min={0}
max={100}
>
<UnitField.DragArea className={DRAG_AREA}>R</UnitField.DragArea>
<UnitField.Input className={INPUT} />
</UnitField.Root>
<UnitField.Root
className={ROOT}
format={pxFormat}
parse={pxParse}
value={bottom}
onValueChange={setBottom}
min={0}
max={100}
>
<UnitField.DragArea className={DRAG_AREA}>B</UnitField.DragArea>
<UnitField.Input className={INPUT} />
</UnitField.Root>
<UnitField.Root
className={ROOT}
format={pxFormat}
parse={pxParse}
value={left}
onValueChange={setLeft}
min={0}
max={100}
>
<UnitField.DragArea className={DRAG_AREA}>L</UnitField.DragArea>
<UnitField.Input className={INPUT} />
</UnitField.Root>
</div>
<div className="mt-4 flex items-center justify-center border border-gray-200 p-4">
<div className="relative inline-block border border-dashed border-gray-300">
<div
style={{
paddingTop: Math.min(top, 32),
paddingRight: Math.min(right, 40),
paddingBottom: Math.min(bottom, 32),
paddingLeft: Math.min(left, 40),
transition: "all 150ms ease",
}}
>
<div className="h-10 w-24 bg-gray-800" />
</div>
<span className="absolute top-1/2 left-1 -translate-y-1/2 font-mono text-[9px] text-gray-400">
{left}
</span>
<span className="absolute top-1/2 right-1 -translate-y-1/2 font-mono text-[9px] text-gray-400">
{right}
</span>
<span className="absolute top-1 left-1/2 -translate-x-1/2 font-mono text-[9px] text-gray-400">
{top}
</span>
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 font-mono text-[9px] text-gray-400">
{bottom}
</span>
</div>
</div>
</div>
);
}