Skip to content
Dethink Components

ComponentsNumberInput

NumberInput

Collect a number with optional minimum, maximum, and step values.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import { NumberInput } from "@dethink/components";

Try the examples, then open the code to use them in your app.

Bounded quantity

type=number gives native min/max/step behavior — arrow keys and spinners step within bounds.

Between 1 and 500 seats.

Show sourceexamples/number-input/basic.tsx
examples/number-input/basic.tsx
"use client";

import {
  Field,
  FieldControl,
  FieldDescription,
  FieldLabel,
  NumberInput,
} from "@dethink/components";

export function NumberInputBasic() {
  return (
    <div className="mx-auto max-w-xs">
      <Field id="ni-seats">
        <FieldLabel>Seats</FieldLabel>
        <FieldControl asChild>
          <NumberInput
            name="seats"
            type="number"
            min={1}
            max={500}
            step={1}
            defaultValue={5}
          />
        </FieldControl>
        <FieldDescription>Between 1 and 500 seats.</FieldDescription>
      </Field>
    </div>
  );
}

Keypad modes and states

numberMode switches the mobile keypad between decimal and digits-only; disabled and invalid states flow through Field.

Show sourceexamples/number-input/modes.tsx
examples/number-input/modes.tsx
"use client";

import {
  Field,
  FieldControl,
  FieldLabel,
  NumberInput,
} from "@dethink/components";

export function NumberInputModes() {
  return (
    <div className="mx-auto grid max-w-md gap-5 sm:grid-cols-2">
      <Field id="ni-amount">
        <FieldLabel>Amount (decimal keypad)</FieldLabel>
        <FieldControl asChild>
          <NumberInput numberMode="decimal" placeholder="19.99" />
        </FieldControl>
      </Field>
      <Field id="ni-code">
        <FieldLabel>Verification code (numeric keypad)</FieldLabel>
        <FieldControl asChild>
          <NumberInput
            numberMode="numeric"
            placeholder="123456"
            maxLength={6}
          />
        </FieldControl>
      </Field>
      <Field id="ni-disabled" disabled>
        <FieldLabel>Disabled</FieldLabel>
        <FieldControl asChild>
          <NumberInput type="number" defaultValue={42} />
        </FieldControl>
      </Field>
      <Field id="ni-invalid" invalid>
        <FieldLabel>Invalid</FieldLabel>
        <FieldControl asChild>
          <NumberInput type="number" defaultValue={-3} min={0} />
        </FieldControl>
      </Field>
    </div>
  );
}

Examples that combine components for common tasks.

Budget allocator

The 100% invariant lives across all three inputs, so every field flips invalid together, a stacked bar visualizes the split, and a live region explains the group-level error.

Budget fully allocated.

Show sourceexamples/number-input/recipe-allocator.tsx
examples/number-input/recipe-allocator.tsx
"use client";

import { useState } from "react";
import {
  Field,
  FieldControl,
  FieldLabel,
  NumberInput,
} from "@dethink/components";

const channels = [
  { id: "search", label: "Search ads" },
  { id: "social", label: "Social" },
  { id: "content", label: "Content" },
] as const;

type Allocation = Record<(typeof channels)[number]["id"], number>;

/**
 * Cross-field validation: each input is fine on its own — the invariant
 * lives across all three, so every field flips invalid together and a live
 * region explains the group-level error.
 */
export function NumberInputRecipeAllocator() {
  const [allocation, setAllocation] = useState<Allocation>({
    search: 50,
    social: 30,
    content: 20,
  });
  const total = channels.reduce(
    (sum, channel) => sum + (allocation[channel.id] || 0),
    0,
  );
  const balanced = total === 100;

  return (
    <div className="mx-auto max-w-sm space-y-4">
      <div className="grid grid-cols-3 gap-3">
        {channels.map((channel) => (
          <Field
            key={channel.id}
            id={`alloc-${channel.id}`}
            invalid={!balanced}
          >
            <FieldLabel className="text-sm">{channel.label}</FieldLabel>
            <FieldControl asChild>
              <NumberInput
                type="number"
                min={0}
                max={100}
                value={allocation[channel.id]}
                onChange={(event) =>
                  setAllocation((current) => ({
                    ...current,
                    [channel.id]: Number(event.target.value) || 0,
                  }))
                }
              />
            </FieldControl>
          </Field>
        ))}
      </div>
      <div className="space-y-1.5">
        <div
          aria-hidden="true"
          className="bg-muted flex h-2 overflow-hidden rounded-full"
        >
          {channels.map((channel, index) => (
            <div
              key={channel.id}
              className={
                index === 0
                  ? "bg-primary"
                  : index === 1
                    ? "bg-info"
                    : "bg-success"
              }
              style={{ width: `${Math.min(allocation[channel.id], 100)}%` }}
            />
          ))}
        </div>
        <p
          aria-live="polite"
          className={`text-sm ${balanced ? "text-muted-foreground" : "text-destructive font-medium"}`}
        >
          {balanced
            ? "Budget fully allocated."
            : `Allocations must total 100% — currently ${total}%.`}
        </p>
      </div>
    </div>
  );
}

NumberInput renders a real input element, so all native numeric attributes apply.

NumberInput props
PropWhat it doesDefault
numberMode"decimal" | "numeric"Sets inputMode so mobile keyboards show the right keypad — decimals or digits only."decimal"
type"text" | "number"Use "number" for native min/max/step semantics and arrow-key stepping; "text" keeps free-form entry with a numeric keypad."text"
controlSize"sm" | "md" | "lg"Height and typography scale aligned with Input."md"
invalidbooleanShows an error style and marks the field as invalid for screen readers.false
…native input propsInputHTMLAttributesRenders a real input: min, max, step, name, disabled, and form behavior are native.Not set