Skip to content

Number Input

A numeric field with − / + steppers and keyboard stepping. Built on Input with spinbutton semantics instead of the native number input, so typing is never fought and values stay predictable.

1 to 20 per order.

Installation

Add Number Input to your project with the CK-UI CLI. It copies the source into your project, so the code is yours to change.

Terminal
npx ck-ui-cli@latest add number-input
Adds
NumberInput.tsxField.tsxInput.tsxLabel.tsxto your CK-UI folder (src/components/ck-ui by default)
Installs
@radix-ui/react-label@radix-ui/react-slotreact-iconsif your project does not have them yet

First time with CK-UI in this project? Run npx ck-ui-cli@latest init once before adding components. The installation guide explains what it sets up.

Manual installationInstall the packages and copy the files yourself.

Install the following dependencies

Run these commands in your terminal to install the required packages

Terminal
npm install react-icons @radix-ui/react-slot @radix-ui/react-label

Add the Input component

NumberInput imports ./Input, so add this file next to it

/components/ui/Input.tsx
"use client";

import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "@/lib/utils";
import { useField, useFieldControlProps } from "./Field";

type InputSize = "sm" | "md" | "lg";

const sizes = {
    root: { sm: "h-8", md: "h-9", lg: "h-10" },
    wrapper: { sm: "gap-1.5 px-2", md: "gap-2 px-2.5", lg: "gap-2 px-3" },
    icon: { sm: "size-3.5", md: "size-4", lg: "size-[18px]" },
    affix: { sm: "px-2", md: "px-2.5", lg: "px-3" },
} as const;

interface InputContextValue {
    size: InputSize;
    invalid: boolean;
}

const InputContext = React.createContext<InputContextValue>({ size: "md", invalid: false });

export interface InputRootProps extends React.HTMLAttributes<HTMLDivElement> {
    size?: InputSize;
    /** Error state: red border and aria-invalid on the input. Also taken from Field.Root. */
    invalid?: boolean;
    /** @deprecated Use `invalid`. */
    error?: boolean;
    asChild?: boolean;
}

const Root = React.forwardRef<HTMLDivElement, InputRootProps>(({ className, children, size = "md", invalid, error, asChild = false, ...props }, ref) => {
    const field = useField();
    const isInvalid = Boolean(invalid ?? error ?? field?.invalid);
    const Component = asChild ? Slot : "div";

    return (
        <InputContext.Provider value={{ size, invalid: isInvalid }}>
            <Component
                ref={ref}
                data-invalid={isInvalid || undefined}
                className={cn(
                    "group/input relative flex w-full overflow-hidden rounded-lg border shadow-x-small transition duration-base ease-standard",
                    sizes.root[size],
                    // Focus: strong border plus a soft halo outside it (field-focus-within), as in CK-UI's original design.
                    isInvalid
                        ? "border-destructive outline-destructive/25 focus-within:border-destructive hover:bg-destructive-subtle focus-within:hover:bg-transparent"
                        : "border-input outline-accent focus-within:border-foreground hover:bg-muted focus-within:hover:bg-transparent",
                    "field-focus-within",
                    "has-[input:disabled]:pointer-events-none has-[input:disabled]:cursor-not-allowed has-[input:disabled]:bg-muted has-[input:disabled]:shadow-none",
                    className
                )}
                {...props}
            >
                {children}
            </Component>
        </InputContext.Provider>
    );
});
Root.displayName = "Input.Root";

export interface InputWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
    asChild?: boolean;
}

// A <div>, not a <label>: affix text must not become part of the input's accessible name.
// Clicking anywhere in the wrapper still focuses the input.
const Wrapper = React.forwardRef<HTMLDivElement, InputWrapperProps>(({ className, asChild = false, onMouseDown, ...props }, ref) => {
    const { size } = React.useContext(InputContext);
    const Component = asChild ? Slot : "div";

    const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
        onMouseDown?.(event);
        if (event.defaultPrevented) return;
        const target = event.target as HTMLElement;
        if (target.closest("input, button, a, select, textarea")) return;
        const input = event.currentTarget.querySelector("input");
        if (input && !input.disabled) {
            event.preventDefault();
            input.focus();
        }
    };

    return (
        <Component
            ref={ref}
            onMouseDown={handleMouseDown}
            className={cn("flex w-full cursor-text items-center transition-colors duration-base", sizes.wrapper[size], className)}
            {...props}
        />
    );
});
Wrapper.displayName = "Input.Wrapper";

export interface InputIconProps extends React.SVGAttributes<SVGElement> {
    /** Icon component, e.g. `as={RiSearchLine}`. */
    as?: React.ElementType;
}

const Icon = React.forwardRef<SVGElement, InputIconProps>(({ as: Component, className, ...props }, ref) => {
    const { size, invalid } = React.useContext(InputContext);

    if (!Component) return null;

    return (
        <Component
            ref={ref}
            aria-hidden="true"
            className={cn(
                "flex shrink-0 select-none items-center justify-center transition duration-base",
                invalid ? "text-destructive" : "text-muted-foreground group-focus-within/input:text-foreground group-has-[input:not(:placeholder-shown)]/input:text-foreground",
                "group-has-[input:disabled]/input:text-muted-foreground/60",
                sizes.icon[size],
                className
            )}
            {...props}
        />
    );
});
Icon.displayName = "Input.Icon";

const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(({ className, type = "text", ...props }, ref) => {
    const { invalid } = React.useContext(InputContext);
    const controlProps = useFieldControlProps(props, { invalid });

    return (
        <input
            ref={ref}
            type={type}
            className={cn(
                "w-full border-none bg-transparent p-0 size-p-sm text-foreground",
                // The wrapper (Input.Root) shows focus; suppress the element's own outline and the forms-plugin ring.
                "outline-none focus:outline-none focus:ring-0",
                "placeholder:text-muted-foreground placeholder:transition-colors",
                "disabled:cursor-not-allowed disabled:text-muted-foreground disabled:placeholder:text-muted-foreground/60",
                className
            )}
            {...controlProps}
        />
    );
});
Input.displayName = "Input.Input";

export interface InputAffixProps extends React.HTMLAttributes<HTMLDivElement> {
    position?: "start" | "end";
}

const Affix = React.forwardRef<HTMLDivElement, InputAffixProps>(({ className, children, position = "start", ...props }, ref) => {
    const { size, invalid } = React.useContext(InputContext);

    return (
        <div
            ref={ref}
            className={cn(
                "flex shrink-0 select-none items-center justify-center size-p-sm transition duration-base",
                invalid ? "text-destructive" : "text-muted-foreground group-has-[input:not(:placeholder-shown)]/input:text-foreground",
                position === "start" ? "border-r border-input" : "border-l border-input",
                sizes.affix[size],
                className
            )}
            {...props}
        >
            {children}
        </div>
    );
});
Affix.displayName = "Input.Affix";

export { Root, Wrapper, Icon, Input, Affix };

Add the Field component

NumberInput imports ./Field, so add this file next to it

/components/ui/Field.tsx
"use client";

import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "@/lib/utils";
import * as Label from "./Label";

/*
 * Field wires a label, description and message to one form control:
 *   Field.Label        → <label for={controlId}>
 *   Field.Description  → referenced by aria-describedby
 *   Field.Message      → referenced by aria-describedby (error text when invalid)
 *   invalid / required / disabled → aria-invalid, required/aria-required, disabled on the control
 * Controls (Input, Textarea, Select, Checkbox, Switch, Radio) read the context through
 * useFieldControlProps. It is plain React state: it works with native forms and form libraries.
 */

type FieldContextValue = {
    controlId: string;
    labelId: string;
    descriptionId: string;
    messageId: string;
    invalid: boolean;
    required: boolean;
    disabled: boolean;
    hasDescription: boolean;
    hasMessage: boolean;
    setHasDescription: (value: boolean) => void;
    setHasMessage: (value: boolean) => void;
};

const FieldContext = React.createContext<FieldContextValue | null>(null);

export function useField() {
    return React.useContext(FieldContext);
}

/**
 * Provided by Fieldset.Root. A native <fieldset disabled> already disables the controls inside it;
 * this lets each Field.Root reflect that state (label styling, `disabled` on the control).
 */
export const FieldsetContext = React.createContext<{ disabled: boolean } | null>(null);

export interface FieldRootProps extends React.HTMLAttributes<HTMLDivElement> {
    /** Id for the control; generated when omitted. */
    id?: string;
    invalid?: boolean;
    required?: boolean;
    disabled?: boolean;
}

const Root = React.forwardRef<HTMLDivElement, FieldRootProps>(({ id, invalid = false, required = false, disabled: disabledProp = false, className, children, ...props }, ref) => {
    const fieldset = React.useContext(FieldsetContext);
    const disabled = disabledProp || Boolean(fieldset?.disabled);
    const generatedId = React.useId();
    const controlId = id ?? `field-${generatedId}`;
    const [hasDescription, setHasDescription] = React.useState(false);
    const [hasMessage, setHasMessage] = React.useState(false);

    const value = React.useMemo<FieldContextValue>(
        () => ({
            controlId,
            labelId: `${controlId}-label`,
            descriptionId: `${controlId}-description`,
            messageId: `${controlId}-message`,
            invalid,
            required,
            disabled,
            hasDescription,
            hasMessage,
            setHasDescription,
            setHasMessage,
        }),
        [controlId, invalid, required, disabled, hasDescription, hasMessage]
    );

    return (
        <FieldContext.Provider value={value}>
            <div
                ref={ref}
                className={cn("flex flex-col gap-1.5", className)}
                data-invalid={invalid || undefined}
                data-disabled={disabled || undefined}
                {...props}
            >
                {children}
            </div>
        </FieldContext.Provider>
    );
});
Root.displayName = "Field.Root";

type FieldLabelProps = React.ComponentPropsWithoutRef<typeof Label.Root>;

const FieldLabel = React.forwardRef<React.ElementRef<typeof Label.Root>, FieldLabelProps>(({ children, ...props }, ref) => {
    const field = useField();

    return (
        <Label.Root ref={ref} id={field?.labelId} htmlFor={field?.controlId} disabled={field?.disabled} {...props}>
            {children}
            {field?.required && <Label.Asterisk />}
        </Label.Root>
    );
});
FieldLabel.displayName = "Field.Label";

const Description = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => {
    const field = useField();
    const setHasDescription = field?.setHasDescription;

    React.useEffect(() => {
        if (!setHasDescription) return;
        setHasDescription(true);
        return () => setHasDescription(false);
    }, [setHasDescription]);

    return <p ref={ref} id={field?.descriptionId} className={cn("size-p-xs text-muted-foreground", className)} {...props} />;
});
Description.displayName = "Field.Description";

export interface FieldMessageProps extends React.HTMLAttributes<HTMLParagraphElement> {
    /** Only render while the field is invalid (default true). */
    showWhenValid?: boolean;
}

const Message = React.forwardRef<HTMLParagraphElement, FieldMessageProps>(({ className, showWhenValid = false, children, ...props }, ref) => {
    const field = useField();
    const visible = Boolean(children) && (showWhenValid || !field || field.invalid);
    const setHasMessage = field?.setHasMessage;

    React.useEffect(() => {
        if (!setHasMessage || !visible) return;
        setHasMessage(true);
        return () => setHasMessage(false);
    }, [setHasMessage, visible]);

    if (!visible) return null;

    return (
        <p
            ref={ref}
            id={field?.messageId}
            aria-live="polite"
            className={cn("size-p-xs", field?.invalid ? "text-destructive-strong" : "text-muted-foreground", className)}
            {...props}
        >
            {children}
        </p>
    );
});
Message.displayName = "Field.Message";

type ControlAttributes = {
    id?: string;
    "aria-describedby"?: string;
    "aria-invalid"?: React.AriaAttributes["aria-invalid"];
    "aria-required"?: React.AriaAttributes["aria-required"];
    required?: boolean;
    disabled?: boolean;
};

/**
 * Merges Field context into a control's props. Explicit props win; describedby ids are combined.
 */
export function useFieldControlProps<P extends ControlAttributes>(props: P, options: { invalid?: boolean } = {}): P & ControlAttributes {
    const field = useField();
    const invalid = Boolean(options.invalid || field?.invalid || props["aria-invalid"] === true || props["aria-invalid"] === "true");

    const describedBy = [props["aria-describedby"], field?.hasDescription ? field.descriptionId : undefined, field?.hasMessage ? field.messageId : undefined]
        .filter(Boolean)
        .join(" ");

    return {
        ...props,
        id: props.id ?? field?.controlId,
        "aria-describedby": describedBy || undefined,
        "aria-invalid": invalid || undefined,
        required: props.required ?? (field?.required || undefined),
        disabled: props.disabled ?? (field?.disabled || undefined),
    };
}

/** Applies Field wiring to any custom control element (single child). */
const Control = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement> & ControlAttributes>((props, ref) => {
    const controlProps = useFieldControlProps(props);
    return <Slot ref={ref} {...controlProps} />;
});
Control.displayName = "Field.Control";

export { Root, FieldLabel as Label, Description, Message, Control };

Add the Label component

NumberInput imports ./Label, so add this file next to it

/components/ui/Label.tsx
"use client";

import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils";

export interface LabelRootProps extends React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> {
    /** Dims the label. The control itself must also be disabled. */
    disabled?: boolean;
}

const Root = React.forwardRef<React.ElementRef<typeof LabelPrimitive.Root>, LabelRootProps>(({ className, disabled, ...props }, ref) => {
    return (
        <LabelPrimitive.Root
            ref={ref}
            data-disabled={disabled || undefined}
            className={cn(
                "group flex cursor-pointer items-center gap-px size-p-sm text-foreground",
                "data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground/60",
                className
            )}
            {...props}
        />
    );
});
Root.displayName = "Label.Root";

// Visual required marker. Screen readers get "required" from the control (required / aria-required).
const Asterisk = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(({ className, children, ...props }, ref) => (
    <span ref={ref} aria-hidden="true" className={cn("text-primary-strong group-data-[disabled]:text-muted-foreground/60", className)} {...props}>
        {children || "*"}
    </span>
));
Asterisk.displayName = "Label.Asterisk";

const Sub = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(({ className, ...props }, ref) => (
    <span ref={ref} className={cn("size-p-xs text-muted-foreground group-data-[disabled]:text-muted-foreground/60", className)} {...props} />
));
Sub.displayName = "Label.Sub";

export { Root, Asterisk, Sub };

Create or update your NumberInput component

Copy this code to create your NumberInput component

/components/ui/NumberInput.tsx
"use client";

import * as React from "react";
import { RiAddLine, RiSubtractLine } from "react-icons/ri";
import { cn } from "@/lib/utils";
import { useField } from "./Field";
import * as Input from "./Input";

/*
 * A numeric field with spinbutton semantics (role="spinbutton", aria-valuenow/min/max), built on Input.
 * Typing is never blocked: "", "-", "1." are allowed while editing; the value is clamped to min/max and
 * normalised when the field is left or Enter is pressed. Keyboard: ArrowUp/Down by `step`, PageUp/PageDown by
 * `largeStep`, Home/End to min/max (when set). The − / + buttons are for pointer users (not tab stops).
 * Plain, locale-independent numbers ("1234.5"); no grouping separators are parsed.
 */

export interface NumberInputProps
    extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "defaultValue" | "onChange" | "size" | "min" | "max" | "step"> {
    value?: number | null;
    defaultValue?: number | null;
    onValueChange?: (value: number | null) => void;
    min?: number;
    max?: number;
    /** Arrow-key and button increment. @default 1 */
    step?: number;
    /** PageUp/PageDown increment. @default step × 10 */
    largeStep?: number;
    size?: "sm" | "md" | "lg";
    invalid?: boolean;
    decrementLabel?: string;
    incrementLabel?: string;
    /** Classes for the outer Input.Root. */
    className?: string;
    inputClassName?: string;
}

const decimals = (value: number) => {
    const text = String(value);
    if (text.includes("e-")) return Number(text.split("e-")[1]);
    return text.includes(".") ? text.split(".")[1].length : 0;
};

const clamp = (value: number, min?: number, max?: number) => Math.min(max ?? Infinity, Math.max(min ?? -Infinity, value));

/** Parses plain numbers only; returns undefined for partial input ("-", ".", "1e"). */
export function parseNumber(text: string): number | null | undefined {
    const trimmed = text.trim();
    if (trimmed === "") return null;
    if (!/^[-+]?(\d+\.?\d*|\.\d+)$/.test(trimmed)) return undefined;
    const parsed = Number(trimmed);
    return Number.isFinite(parsed) ? parsed : undefined;
}

const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
    (
        {
            value: valueProp,
            defaultValue = null,
            onValueChange,
            min,
            max,
            step = 1,
            largeStep,
            size = "md",
            invalid,
            disabled,
            readOnly,
            decrementLabel = "Decrease",
            incrementLabel = "Increase",
            className,
            inputClassName,
            id,
            onBlur,
            onKeyDown,
            ...props
        },
        ref
    ) => {
        const field = useField();
        const generatedId = React.useId();
        const inputId = id ?? field?.controlId ?? `number-${generatedId}`;
        const [uncontrolled, setUncontrolled] = React.useState<number | null>(defaultValue);
        const isControlled = valueProp !== undefined;
        const value = isControlled ? valueProp : uncontrolled;
        const [text, setText] = React.useState(value === null ? "" : String(value));
        const textRef = React.useRef(text);
        textRef.current = text;
        const editingRef = React.useRef(false);
        const isDisabled = Boolean(disabled ?? field?.disabled);
        const interactive = !isDisabled && !readOnly;

        // Follow external value changes unless the user is mid-edit with an equivalent value ("1." vs 1).
        React.useEffect(() => {
            if (editingRef.current && parseNumber(textRef.current) === value) return;
            setText(value === null ? "" : String(value));
        }, [value]);

        const setValue = (next: number | null) => {
            if (!isControlled) setUncontrolled(next);
            if (next !== value) onValueChange?.(next);
        };

        const precision = (base: number) => Math.max(decimals(step), decimals(base));

        const stepBy = (amount: number) => {
            if (!interactive) return;
            const base = value ?? (amount > 0 ? (min ?? 0) - step : (max ?? 0) + step);
            const raw = clamp(base + amount, min, max);
            const next = Number(raw.toFixed(precision(base)));
            setValue(next);
            setText(String(next));
        };

        const commit = () => {
            editingRef.current = false;
            const parsed = parseNumber(text);
            if (parsed === undefined) {
                setText(value === null ? "" : String(value));
                return;
            }
            const next = parsed === null ? null : clamp(parsed, min, max);
            setValue(next);
            setText(next === null ? "" : String(next));
        };

        const allowNegative = min === undefined || min < 0;
        const allowDecimal = !Number.isInteger(step);

        const buttonClasses =
            "flex h-full w-9 shrink-0 items-center justify-center text-muted-foreground transition-colors duration-fast hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-40";

        return (
            <Input.Root size={size} invalid={invalid} className={cn("w-auto", className)}>
                <button
                    type="button"
                    tabIndex={-1}
                    aria-label={decrementLabel}
                    aria-controls={inputId}
                    disabled={!interactive || (value !== null && min !== undefined && value <= min)}
                    onMouseDown={(event) => event.preventDefault()}
                    onClick={() => stepBy(-step)}
                    className={cn(buttonClasses, "border-r border-input")}
                >
                    <RiSubtractLine aria-hidden="true" className="size-4" />
                </button>
                <Input.Wrapper className="px-2">
                    <Input.Input
                        ref={ref}
                        id={inputId}
                        role="spinbutton"
                        inputMode={allowNegative ? "text" : allowDecimal ? "decimal" : "numeric"}
                        autoComplete="off"
                        aria-valuenow={value ?? undefined}
                        aria-valuemin={min}
                        aria-valuemax={max}
                        value={text}
                        disabled={disabled}
                        readOnly={readOnly}
                        onChange={(event) => {
                            editingRef.current = true;
                            setText(event.target.value);
                            const parsed = parseNumber(event.target.value);
                            // Report complete numbers as they are typed; clamping waits for commit.
                            if (parsed !== undefined) setValue(parsed);
                        }}
                        onBlur={(event) => {
                            commit();
                            onBlur?.(event);
                        }}
                        onKeyDown={(event) => {
                            onKeyDown?.(event);
                            if (event.defaultPrevented || !interactive) return;
                            const big = largeStep ?? step * 10;
                            const actions: Record<string, () => void> = {
                                ArrowUp: () => stepBy(step),
                                ArrowDown: () => stepBy(-step),
                                PageUp: () => stepBy(big),
                                PageDown: () => stepBy(-big),
                                Enter: commit,
                            };
                            const jumpTo = (target: number) => {
                                setValue(target);
                                setText(String(target));
                            };
                            if (min !== undefined) actions.Home = () => jumpTo(min);
                            if (max !== undefined) actions.End = () => jumpTo(max);
                            const action = actions[event.key];
                            if (!action) return;
                            if (event.key !== "Enter") event.preventDefault();
                            editingRef.current = false;
                            action();
                        }}
                        className={cn("text-center tabular-nums", inputClassName)}
                        {...props}
                    />
                </Input.Wrapper>
                <button
                    type="button"
                    tabIndex={-1}
                    aria-label={incrementLabel}
                    aria-controls={inputId}
                    disabled={!interactive || (value !== null && max !== undefined && value >= max)}
                    onMouseDown={(event) => event.preventDefault()}
                    onClick={() => stepBy(step)}
                    className={cn(buttonClasses, "border-l border-input")}
                >
                    <RiAddLine aria-hidden="true" className="size-4" />
                </button>
            </Input.Root>
        );
    }
);
NumberInput.displayName = "NumberInput.Root";

export { NumberInput as Root };

Test Your Component

  • 1. Add the component to a test page
  • 2. Check all props and variants work as expected
  • 3. Verify responsive behavior
  • 4. Test with different content/data
  • 5. Ensure accessibility features work correctly

Percentage

Controlled value with a step of 5 and a large step of 25 for PageUp/PageDown.

Arrows: ±5 · PageUp/PageDown: ±25

Decimals and Read-only

Decimal steps are rounded to the step's precision (no 0.30000000000000004). readOnly keeps the value visible and focusable but unchangeable.

−20 to 40, in 0.5° steps. Value: -2.5

Typing and Parsing

  • Partial input such as "-", "." or "1." is allowed while typing; complete numbers are reported as you type.
  • On blur or Enter the value is clamped to min/max; text that is not a number reverts to the last valid value; an empty field becomes null.
  • Only plain numbers are parsed ("1234.5"). Grouping separators and exponents ("1,000", "1e3") are not, so there are no locale surprises.

Accessibility

  • The input has role="spinbutton" with aria-valuenow, aria-valuemin and aria-valuemax, and is labelled by Field.Label.
  • ArrowUp/ArrowDown change by step, PageUp/PageDown by the large step, Home/End go to min/max when they are set.
  • The − / + buttons are labelled ("Decrease", "Increase"), disabled at the limits and kept out of the tab order since the keyboard already steps.
  • The mobile keyboard follows the range: numeric for whole positive numbers, decimal for decimal steps, full keyboard when negative values are allowed (many numeric keypads have no minus key).

API Reference

Import with import * as NumberInput from "@/components/ui/NumberInput". Other input attributes go to the input; ref points at it.

NumberInput.Root

PropTypeDefaultDescription
value / defaultValue"number | null""null"

The number; null when empty.

onValueChange"(value: number | null) => void""undefined"

Complete numbers while typing, final value on commit.

min / max"number""undefined"

Limits (clamped on commit and when stepping).

step"number""1"

Arrow-key and button increment.

largeStep"number""step × 10"

PageUp/PageDown increment.

readOnly / disabled"boolean""false"

Prevent changes; disabled also reads Field.

invalid"boolean""from Field"

Error state.

decrementLabel / incrementLabel"string""Decrease / Increase"

Button names.

size
"sm""md""lg"
"md"

Height.

className"string""undefined"

Classes for the outer Input.Root (e.g. width).

parseNumber(text)

The parser NumberInput uses: a number, null for empty text, undefined for partial or invalid text.

PropTypeDefaultDescription
text"string""-"

Raw input text.

Used in these blocks

Block families with at least one block built on this component. Each page previews its blocks and lists the components and packages they use.

Application