Quantity: 3 / 10
"use client"
import * as React from "react"
import { NumberField } from "@/components/ui/number-field"
export default function NumberFieldDemo() {
const [value, setValue] = React.useState(3)
return (
<div className="flex flex-col items-center gap-3">
<NumberField
value={value}
onValueChange={setValue}
min={0}
max={10}
aria-label="Quantity"
/>
<p className="text-muted-foreground text-sm">Quantity: {value} / 10</p>
</div>
)
}Installation
npx logic2b@latest add number-fieldWorking with an AI assistant? Copy this prompt into Claude Code, Cursor or Copilot — it carries the install steps, a no-CLI fallback and a verification checklist.
Add the "number-field" item from the logic2b ui registry (https://ui.logic2b.com) to this project. Follow every step and verify at the end. ## Steps 1. If the project has no `components.json` yet, set up logic2b ui first: ```bash npx logic2b@latest init ``` 2. Install the item (the CLI resolves registry dependencies automatically): ```bash npx logic2b@latest add number-field ``` 3. Install any npm dependencies the command prints. ## If the CLI is not available Do it manually from the registry payload: 1. Fetch https://ui.logic2b.com/r/number-field.json. 2. Write every entry in `files[]` to its mapped project path (see below). 3. Repeat (recursively) for each name in `registryDependencies`. 4. Install every package listed in `dependencies` across those payloads. ## Registry reference - Index of every item: https://ui.logic2b.com/r/index.json - Item payload (full source): https://ui.logic2b.com/r/<name>.json - Usage examples per item: https://ui.logic2b.com/r/demos/index.json → https://ui.logic2b.com/r/demos/<demo>.json - Docs index for agents: https://ui.logic2b.com/llms.txt (full docs: https://ui.logic2b.com/llms-full.txt) - Every docs page is Markdown when you append `.md` to its URL. Registry file paths map to project paths like this: - `ui/*` → `@/components/ui/*` - `blocks/<name>/*` → `@/components/*` - `charts/*` → `@/components/charts/*` - `hooks/*` → `@/hooks/*` - `lib/utils.ts` → `@/lib/utils.ts` - `theme.css` → next to the project's Tailwind entry stylesheet ## Conventions (do not break these) - Tailwind CSS v4 (CSS-first config; no tailwind.config.js needed) and React 19. - All colors go through semantic tokens (`bg-primary`, `text-muted-foreground`, `border-border`, …). Never hardcode hex/oklch values in components. - Dark mode is class-based: toggle `.dark` on `<html>`. - Icons come from lucide-react. ## Verify before finishing 1. The dev server starts and the app renders without console errors. 2. TypeScript compiles (`tsc --noEmit` or the framework's check). 3. Components use the theme: a `<Button>` shows the primary token color, and toggling `.dark` on `<html>` switches the palette.
Install the dependencies:
npm install lucide-reactCopy into ui/number-field.tsx:
"use client"
import * as React from "react"
import { MinusIcon, PlusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
export interface NumberFieldProps
extends Omit<
React.ComponentProps<"input">,
"value" | "defaultValue" | "onChange" | "type"
> {
value?: number
defaultValue?: number
onValueChange?: (value: number) => void
min?: number
max?: number
step?: number
}
/**
* A numeric input with decrement/increment steppers, min/max clamping and step.
* Controlled via `value`/`onValueChange` or uncontrolled via `defaultValue`.
* Renders a real `<input type="number">` so native form submission and keyboard
* work; the native spinners are hidden in favor of the styled buttons.
*/
function NumberField({
className,
value,
defaultValue,
onValueChange,
min,
max,
step = 1,
disabled,
...props
}: NumberFieldProps) {
const isControlled = value !== undefined
const [internal, setInternal] = React.useState(defaultValue ?? 0)
const current = isControlled ? (value as number) : internal
const clamp = (n: number) => {
if (min !== undefined) n = Math.max(min, n)
if (max !== undefined) n = Math.min(max, n)
return n
}
const commit = (n: number) => {
const next = Number.isNaN(n) ? NaN : clamp(n)
if (!isControlled) setInternal(next)
onValueChange?.(next)
}
const stepBy = (direction: 1 | -1) => {
const base = Number.isNaN(current) ? (min ?? 0) : current
// Trim floating-point noise from repeated steps (0.1 + 0.2 …).
const raw = base + direction * step
commit(Math.round((raw + Number.EPSILON) * 1e6) / 1e6)
}
const atMin = min !== undefined && !Number.isNaN(current) && current <= min
const atMax = max !== undefined && !Number.isNaN(current) && current >= max
const buttonClass =
"flex h-full aspect-square items-center justify-center text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-40 focus-visible:text-foreground outline-none"
return (
<div
data-slot="number-field"
data-disabled={disabled ? "" : undefined}
className={cn(
"border-input focus-within:border-ring focus-within:ring-ring/50 inline-flex h-9 items-center rounded-md border bg-transparent shadow-xs transition-[color,box-shadow] focus-within:ring-[3px]",
"data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
>
<button
type="button"
data-slot="number-field-decrement"
aria-label="Decrement"
tabIndex={-1}
disabled={disabled || atMin}
onClick={() => stepBy(-1)}
className={cn(buttonClass, "border-r")}
>
<MinusIcon className="size-4" />
</button>
<input
type="number"
inputMode="decimal"
data-slot="number-field-input"
disabled={disabled}
min={min}
max={max}
step={step}
value={Number.isNaN(current) ? "" : current}
onChange={(e) =>
commit(e.target.value === "" ? NaN : Number(e.target.value))
}
className={cn(
"h-full w-16 min-w-0 bg-transparent text-center text-sm tabular-nums outline-none",
"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
)}
{...props}
/>
<button
type="button"
data-slot="number-field-increment"
aria-label="Increment"
tabIndex={-1}
disabled={disabled || atMax}
onClick={() => stepBy(1)}
className={cn(buttonClass, "border-l")}
>
<PlusIcon className="size-4" />
</button>
</div>
)
}
export { NumberField }Usage
import { NumberField } from "@/components/ui/number-field"
<NumberField defaultValue={1} min={0} max={10} />
Controlled:
const [value, setValue] = React.useState(3)
<NumberField value={value} onValueChange={setValue} min={0} max={10} />
It renders a real <input type="number">, so it works with native form
submission and keyboard entry; the browser’s own spinners are hidden in favor of
the styled buttons, which disable themselves at the min/max bounds.
Examples
Step
import { NumberField } from "@/components/ui/number-field"
export default function NumberFieldStepDemo() {
return (
<NumberField defaultValue={1.5} step={0.5} min={0} aria-label="Amount" />
)
}API
| Prop | Type | Default |
|---|---|---|
value |
number |
— (controlled) |
defaultValue |
number |
0 |
onValueChange |
(value: number) => void |
— |
min |
number |
— |
max |
number |
— |
step |
number |
1 |
disabled |
boolean |
false |
| …props | React.ComponentProps<"input"> |
— |