Components
- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- CommandNew
- Dialog
- Empty
- Field
- Fieldset
- Form
- Frame
- Group
- Input
- Input Group
- Kbd
- Label
- Menu
- Meter
- Number Field
- Pagination
- Popover
- Preview Card
- Progress
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- Skeleton
- Slider
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Toolbar
- Tooltip
Resources
Number Field
A numeric input element with increment and decrement buttons, and a scrub area.
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<NumberField defaultValue={0}>
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}
Installation
pnpm dlx shadcn@latest add @coss/number-field
Usage
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldScrubArea,
} from "@/components/ui/number-field"<NumberField defaultValue={0}>
<NumberFieldScrubArea label="Quantity" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>API Reference
NumberField
Root component. Styled wrapper for NumberField.Root from Base UI.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "lg" | "default" | Controls the size of all child components via context |
NumberFieldGroup
Container for the input and buttons. Styled wrapper for NumberField.Group from Base UI.
NumberFieldInput
Input element for the number value. Styled wrapper for NumberField.Input from Base UI.
NumberFieldIncrement
Button to increment the value. Styled wrapper for NumberField.Increment from Base UI with plus icon.
NumberFieldDecrement
Button to decrement the value. Styled wrapper for NumberField.Decrement from Base UI with minus icon.
NumberFieldScrubArea
Draggable area for scrubbing the value. Styled wrapper for NumberField.ScrubArea from Base UI.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Label text displayed in the scrub area |
Examples
For accessible labelling and validation, prefer using the Field component to wrap number fields. See the related example: Number field.
Small Size
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<NumberField defaultValue={0} size="sm">
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}
Large Size
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<NumberField defaultValue={0} size="lg">
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}
Disabled
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<NumberField defaultValue={42} disabled>
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}
With External Label
import { useId } from "react";
import { Label } from "@/components/ui/label";
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function Particle() {
const id = useId();
return (
<div className="flex flex-col items-start gap-2">
<Label htmlFor={id}>Quantity</Label>
<NumberField defaultValue={0} id={id}>
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</div>
);
}
With Scrub
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldScrubArea,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<NumberField defaultValue={0}>
<NumberFieldScrubArea label="Quantity" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}
With Range
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<NumberField defaultValue={5} max={10} min={0}>
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}
With Formatted Value
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<NumberField
defaultValue={0}
format={{ currency: "USD", style: "currency" }}
>
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}
With Step
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldScrubArea,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<div className="flex flex-col gap-6">
<NumberField defaultValue={0} step={10}>
<NumberFieldScrubArea label="Step 10" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
<NumberField defaultValue={0} step={0.1}>
<NumberFieldScrubArea label="Step 0.1" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</div>
);
}
Form Integration
"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Field } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldScrubArea,
} from "@/components/ui/number-field";
const schema = z.object({
quantity: z.coerce
.number({ message: "Please enter a quantity." })
.min(1, { message: "Quantity must be at least 1." })
.max(100, { message: "Maximum quantity is 100." }),
});
type Errors = Record<string, string | string[]>;
async function submitForm(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const result = schema.safeParse(Object.fromEntries(formData));
if (!result.success) {
const { fieldErrors } = z.flattenError(result.error);
return { errors: fieldErrors as Errors };
}
return {
data: result.data,
errors: {} as Errors,
};
}
export default function Particle() {
const [loading, setLoading] = useState(false);
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
setLoading(true);
const response = await submitForm(event);
await new Promise((r) => setTimeout(r, 800));
setLoading(false);
if (Object.keys(response.errors).length === 0) {
alert(`Quantity: ${response.data?.quantity}`);
}
};
return (
<Form className="max-w-64" onSubmit={onSubmit}>
<Field name="quantity">
<NumberField defaultValue={1} disabled={loading} max={100} min={1}>
<NumberFieldScrubArea label="Quantity" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</Field>
<Button disabled={loading} type="submit">
Submit
</Button>
</Form>
);
}