Siteplane UI

Number Field

Edit a numeric value with typing, stepping or scrubbing.

Markdown
"use client";

import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
  NumberFieldScrubArea,
} from "@/siteplane/components/number-field";

export default function Example() {
  return (
    <NumberField defaultValue={1}>
      <NumberFieldScrubArea label="Quantity" />
      <NumberFieldGroup>
        <NumberFieldDecrement aria-label="Decrease quantity" />
        <NumberFieldInput />
        <NumberFieldIncrement aria-label="Increase quantity" />
      </NumberFieldGroup>
    </NumberField>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/number-field

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input

Field is installed with Input.

Usage

import {
  NumberField,
  NumberFieldScrubArea,
  NumberFieldGroup,
  NumberFieldDecrement,
  NumberFieldInput,
  NumberFieldIncrement,
} from "@/siteplane/components/number-field";

<NumberField defaultValue={1}>
  <NumberFieldScrubArea label="Quantity" />
  <NumberFieldGroup>
    <NumberFieldDecrement aria-label="Decrease quantity" />
    <NumberFieldInput />
    <NumberFieldIncrement aria-label="Increase quantity" />
  </NumberFieldGroup>
</NumberField>;

Composition

NumberField
├── NumberFieldScrubArea
└── NumberFieldGroup
    ├── NumberFieldDecrement
    ├── NumberFieldInput
    └── NumberFieldIncrement

Controlled range

Use a number or null for an empty field. Min, max and step constrain edits; the application stores the value.

"use client";

import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
  NumberFieldScrubArea,
} from "@/siteplane/components/number-field";
import { useState } from "react";

export default function Example() {
  const [value, setValue] = useState<number | null>(30);

  return (
    <NumberField
      value={value}
      onValueChange={setValue}
      min={5}
      max={120}
      step={5}
    >
      <NumberFieldScrubArea label="Timeout (minutes)" />
      <NumberFieldGroup>
        <NumberFieldDecrement aria-label="Decrease timeout" />
        <NumberFieldInput />
        <NumberFieldIncrement aria-label="Increase timeout" />
      </NumberFieldGroup>
    </NumberField>
  );
}

Formatted value

Locale and format apply to the editable value. The value remains numeric.

"use client";

import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
  NumberFieldScrubArea,
} from "@/siteplane/components/number-field";

export default function Example() {
  return (
    <NumberField
      defaultValue={12.5}
      min={0}
      format={{ style: "currency", currency: "USD" }}
      locale="en-US"
    >
      <NumberFieldScrubArea label="Budget" />
      <NumberFieldGroup>
        <NumberFieldDecrement aria-label="Decrease budget" />
        <NumberFieldInput />
        <NumberFieldIncrement aria-label="Increase budget" />
      </NumberFieldGroup>
    </NumberField>
  );
}

Variants and disabled

variant sets the control surface. Disabled prevents stepping and editing.

"use client";

import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
  NumberFieldScrubArea,
} from "@/siteplane/components/number-field";

const fields: {
  disabled?: boolean;
  label: string;
  variant?: "default" | "elevated" | "inset";
}[] = [
  { label: "default", variant: "default" },
  { label: "elevated", variant: "elevated" },
  { label: "inset", variant: "inset" },
  { label: "disabled", disabled: true },
];

export default function Example() {
  return (
    <div className="grid gap-4">
      {fields.map(({ disabled, label, variant }) => (
        <NumberField
          defaultValue={2}
          disabled={disabled}
          key={label}
          variant={variant}
        >
          <NumberFieldScrubArea label={`Quantity, ${label}`} />
          <NumberFieldGroup>
            <NumberFieldDecrement aria-label="Decrease quantity" />
            <NumberFieldInput />
            <NumberFieldIncrement aria-label="Increase quantity" />
          </NumberFieldGroup>
        </NumberField>
      ))}
    </div>
  );
}

Sizes

Omit size to inherit the provider's control size. textSize overrides the text size separately.

"use client";

import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
  NumberFieldScrubArea,
} from "@/siteplane/components/number-field";

const sizes = [
  { size: "sm", label: "small" },
  { size: "md", label: "medium" },
  { size: "lg", label: "large" },
  { size: "xl", label: "extra large" },
] as const;

export default function Example() {
  return (
    <div className="grid gap-4">
      {sizes.map(({ size, label }) => (
        <NumberField key={size} size={size} defaultValue={2}>
          <NumberFieldScrubArea label={`Quantity, ${label}`} />
          <NumberFieldGroup>
            <NumberFieldDecrement aria-label="Decrease quantity" />
            <NumberFieldInput />
            <NumberFieldIncrement aria-label="Increase quantity" />
          </NumberFieldGroup>
        </NumberField>
      ))}
    </div>
  );
}

Emphasized value

Use emphasized when the number is the content, such as a quantity in a cart or the seats of a plan. The value turns semibold and one text step larger. In a form with other fields, keep the regular value.

"use client";

import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
  NumberFieldScrubArea,
} from "@/siteplane/components/number-field";

export default function Example() {
  return (
    <NumberField emphasized defaultValue={2} min={1} max={10}>
      <NumberFieldScrubArea label="Quantity" />
      <NumberFieldGroup>
        <NumberFieldDecrement aria-label="Decrease quantity" />
        <NumberFieldInput />
        <NumberFieldIncrement aria-label="Increase quantity" />
      </NumberFieldGroup>
    </NumberField>
  );
}

Form integration

Give the Field a name so Form submits the numeric value. min and max clamp the value, and FieldDescription explains the range. The field is compact by default; add className="w-full" to fill the form column.

Choose between 1 and 100 seats.

"use client";

import { Button } from "@/siteplane/components/button";
import { Field, FieldDescription } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
  NumberFieldScrubArea,
} from "@/siteplane/components/number-field";

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-64 gap-4"
      onSubmit={(event) => event.preventDefault()}
    >
      <Field name="seats">
        <NumberField className="w-full" defaultValue={5} min={1} max={100}>
          <NumberFieldScrubArea label="Team seats" />
          <NumberFieldGroup>
            <NumberFieldDecrement aria-label="Remove a seat" />
            <NumberFieldInput />
            <NumberFieldIncrement aria-label="Add a seat" />
          </NumberFieldGroup>
        </NumberField>
        <FieldDescription>Choose between 1 and 100 seats.</FieldDescription>
      </Field>
      <Button type="submit">Update seats</Button>
    </Form>
  );
}

API reference

Component properties
PropTypeDefaultDescription
NumberField.sizesm | md | lg | xlprovider / lgShared dimensions.
NumberField.textSizesm | basebase for xl; otherwise CSSGroup text size.
NumberField.emphasizedbooleanfalseSemibold value one text step larger than the field text: 16 px at sm, 18 px at md and lg, 20 px at xl. Overrides textSize.
NumberField.variantdefault | elevated | insetdefaultGroup surface.
NumberField.idstringgeneratedInput/label association.
NumberField.classNamestringcompact widthLayout and width. By default the field is two step buttons plus a 7rem value field wide (11rem at `md`); a width utility such as w-full or w-64 replaces that.
NumberFieldScrubArea.labelstringrequiredVisible associated label; scrubbing changes the number.
value / defaultValuenumber | nulluncontrolledControlled or initial numeric value.
onValueChange(value, eventDetails) => voidunsetNumeric edit callback.
onValueCommitted(value, eventDetails) => voidunsetCommitted numeric edit callback.

Root numeric range, formatting, locale, required, disabled, readOnly and form props follow Base UI Number Field. NumberFieldGroup owns the shared surface; NumberFieldInput owns text editing; Decrement/Increment use ghost icon Buttons by default and accept render, but supply their own minus/plus children. ScrubArea must be inside NumberField and supplies its own cursor. All styled parts accept string className.

NumberFieldPrimitive exports the foundation namespace. NumberFieldContext exports {fieldId, size, textSize?, variant} | null, default null. CursorGrowIcon accepts SVG props, defaults aria-hidden true and supplies the scrub cursor; it is not a control.

Read the installed source.

Usage notes

Arrow keys adjust the focused number. The field is compact by default, sized for quantities and counts; set a width in className for longer values. A value longer than the field ends with an ellipsis until the field has focus; then it scrolls to the caret, as in any input. Keep a visible label and accessible names for step buttons. For invalid or server-rejected values, compose Field help and errors; the numeric component does not persist or validate business rules.