arikchakma

Unit Field

Component 1

About

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

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Unit Field to your project and fits it to your theme.

Code

How Unit Field is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"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;
}

function degFormat(v: number) {
  return `${v}°`;
}

function degParse(v: string) {
  const n = parseInt(v.replace("°", ""), 10);
  return isNaN(n) ? null : n;
}

function pctFormat(v: number) {
  return `${v}%`;
}

function pctParse(v: string) {
  const n = parseInt(v.replace("%", ""), 10);
  return isNaN(n) ? null : n;
}

function FieldRow(props: { label: string; children: React.ReactNode }) {
  return (
    <div className="grid grid-cols-[80px_1fr] items-center gap-3">
      <span className="text-[13px] text-gray-400">{props.label}</span>
      {props.children}
    </div>
  );
}

export default function UnitFieldPropertiesDemo() {
  const [width, setWidth] = React.useState(200);
  const [height, setHeight] = React.useState(120);
  const [radius, setRadius] = React.useState(0);
  const [rotation, setRotation] = React.useState(0);
  const [opacity, setOpacity] = React.useState(100);
  const [border, setBorder] = React.useState(1);

  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">01</span>
          <h3 className="text-[14px] font-medium text-black">Properties</h3>
        </div>
        <p className="mt-1 text-[13px] text-gray-500">
          Drag the labels to scrub. Arrow keys for fine control, shift for large
          steps.
        </p>
      </header>

      <div className="space-y-1.5">
        <FieldRow label="Width">
          <UnitField.Root
            className={ROOT}
            format={pxFormat}
            parse={pxParse}
            value={width}
            onValueChange={setWidth}
            min={0}
            max={1000}
          >
            <UnitField.DragArea className={DRAG_AREA}>W</UnitField.DragArea>
            <UnitField.Input className={INPUT} />
          </UnitField.Root>
        </FieldRow>
        <FieldRow label="Height">
          <UnitField.Root
            className={ROOT}
            format={pxFormat}
            parse={pxParse}
            value={height}
            onValueChange={setHeight}
            min={0}
            max={1000}
          >
            <UnitField.DragArea className={DRAG_AREA}>H</UnitField.DragArea>
            <UnitField.Input className={INPUT} />
          </UnitField.Root>
        </FieldRow>
        <FieldRow label="Radius">
          <UnitField.Root
            className={ROOT}
            format={pxFormat}
            parse={pxParse}
            value={radius}
            onValueChange={setRadius}
            min={0}
            max={999}
          >
            <UnitField.DragArea className={DRAG_AREA}>R</UnitField.DragArea>
            <UnitField.Input className={INPUT} />
          </UnitField.Root>
        </FieldRow>
        <FieldRow label="Rotation">
          <UnitField.Root
            className={ROOT}
            format={degFormat}
            parse={degParse}
            value={rotation}
            onValueChange={setRotation}
            min={0}
            max={360}
          >
            <UnitField.DragArea className={DRAG_AREA}>&deg;</UnitField.DragArea>
            <UnitField.Input className={INPUT} />
          </UnitField.Root>
        </FieldRow>
        <FieldRow label="Opacity">
          <UnitField.Root
            className={ROOT}
            format={pctFormat}
            parse={pctParse}
            value={opacity}
            onValueChange={setOpacity}
            min={0}
            max={100}
          >
            <UnitField.DragArea className={DRAG_AREA}>%</UnitField.DragArea>
            <UnitField.Input className={INPUT} />
          </UnitField.Root>
        </FieldRow>
        <FieldRow label="Border">
          <UnitField.Root
            className={ROOT}
            format={pxFormat}
            parse={pxParse}
            value={border}
            onValueChange={setBorder}
            min={0}
            max={20}
          >
            <UnitField.DragArea className={DRAG_AREA}>B</UnitField.DragArea>
            <UnitField.Input className={INPUT} />
          </UnitField.Root>
        </FieldRow>
      </div>

      <div className="mt-4 flex h-32 items-center justify-center overflow-hidden border border-gray-200">
        <div
          className="bg-gray-800"
          style={{
            width: Math.min(width, 160),
            height: Math.min(height, 80),
            borderRadius: radius,
            transform: `rotate(${rotation}deg)`,
            opacity: opacity / 100,
            borderWidth: border,
            borderColor: "#9ca3af",
            borderStyle: "solid",
            transition: "all 150ms ease",
          }}
        />
      </div>
    </div>
  );
}

More by arikchakma

View all
  • Cromia Color Picker

    Cromia Color Picker

    42
  • Cromia Compact

    Cromia Compact

    18
  • Cromia Channels

    Cromia Channels

    14
  • Cromia Swatches

    Cromia Swatches

    10