Number Field
A decrement/input/increment stepper, wrapping Morphos's NumberField primitive.
Installation
npx kosmesis add number-fieldpnpm dlx kosmesis add number-fieldyarn dlx kosmesis add number-fieldbunx kosmesis add number-fieldInstall the following dependencies:
npm install @morphos/inputspnpm add @morphos/inputsyarn add @morphos/inputsbun add @morphos/inputsCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { NumberField as MorphosNumberField, type NumberFieldProps as MorphosNumberFieldProps } from "@morphos/inputs";import { cn } from "@/lib/utils";export type NumberFieldProps = MorphosNumberFieldProps;/** * Morphos's `NumberField` is a single self-contained primitive (decrement button, native * `<input type="text" inputmode="decimal">`, increment button) — no compound Trigger/Content * parts, so (like `Slider`) it's composed here rather than subclassed. */@Component()export class NumberField extends StatelessComponent<NumberFieldProps> { render() { const { class: cls, ...rest } = this.props; return ( <MorphosNumberField class={cn( "flex h-9 w-fit items-stretch rounded-md border border-input shadow-xs transition-[color,box-shadow]", "focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50", "data-disabled:pointer-events-none data-disabled:opacity-50", "[&_button]:inline-flex [&_button]:w-9 [&_button]:shrink-0 [&_button]:items-center [&_button]:justify-center [&_button]:rounded-none [&_button]:text-sm [&_button]:text-muted-foreground [&_button]:outline-none", "[&_button:first-child]:rounded-l-md [&_button:last-child]:rounded-r-md", "[&_button:hover:not(:disabled)]:bg-accent [&_button:hover:not(:disabled)]:text-accent-foreground", "[&_button:disabled]:pointer-events-none [&_button:disabled]:opacity-50", "[&_input]:h-full [&_input]:min-w-0 [&_input]:flex-1 [&_input]:border-0 [&_input]:bg-transparent [&_input]:px-1 [&_input]:text-center [&_input]:text-sm [&_input]:outline-none [&_input]:disabled:cursor-not-allowed", cls, )} {...rest} /> ); }}Install the following dependencies:
npm install @morphos/inputs @praxisjs/csspnpm add @morphos/inputs @praxisjs/cssyarn add @morphos/inputs @praxisjs/cssbun add @morphos/inputs @praxisjs/cssCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { NumberField as MorphosNumberField, type NumberFieldProps as MorphosNumberFieldProps } from "@morphos/inputs";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class NumberFieldStyles extends Stylesheet { $root = this.css({ display: "flex", height: "2.25rem", width: "fit-content", alignItems: "stretch", borderRadius: `calc(${t.radius} - 2px)`, border: `1px solid ${t.input}`, boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", transition: "color 120ms ease, box-shadow 120ms ease", }) .on("&:focus-within", { borderColor: t.ring, boxShadow: `0 0 0 3px color-mix(in oklab, ${t.ring} 50%, transparent)`, }) .on("&[data-disabled]", { pointerEvents: "none", opacity: 0.5 }) .on("& button", { display: "inline-flex", width: "2.25rem", flexShrink: 0, alignItems: "center", justifyContent: "center", borderRadius: "0", fontSize: "0.875rem", color: t.mutedForeground, outline: "none", }) .on("& button:first-child", { borderTopLeftRadius: `calc(${t.radius} - 2px)`, borderBottomLeftRadius: `calc(${t.radius} - 2px)`, }) .on("& button:last-child", { borderTopRightRadius: `calc(${t.radius} - 2px)`, borderBottomRightRadius: `calc(${t.radius} - 2px)`, }) .on("& button:hover:not(:disabled)", { backgroundColor: t.accent, color: t.accentForeground }) .on("& button:disabled", { pointerEvents: "none", opacity: 0.5 }) .on("& input", { height: "100%", minWidth: "0", flex: "1 1 0%", border: "0", backgroundColor: "transparent", padding: "0 0.25rem", textAlign: "center", fontSize: "0.875rem", outline: "none", }) .on("& input:disabled", { cursor: "not-allowed" });}export type NumberFieldProps = MorphosNumberFieldProps;/** * Morphos's `NumberField` is a single self-contained primitive (decrement button, native * `<input type="text" inputmode="decimal">`, increment button) — no compound Trigger/Content * parts, so (like `Slider`) it's composed here rather than subclassed. */@Component()export class NumberField extends StatelessComponent<NumberFieldProps> { @Styled(NumberFieldStyles) $s!: NumberFieldStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosNumberField class={cx(this.$s.$root, cls)} {...rest} />; }}Examples
Usage
import { NumberField } from "@/components/ui/number-field";
<NumberField defaultValue={3} min={0} max={10} step={1} aria-label="Quantity" />Formatting
formatOptions takes an Intl.NumberFormat options object — the displayed text is formatted with
it, while the underlying value stays a plain number:
<NumberField
defaultValue={19.99}
min={0}
step={0.01}
formatOptions={{ style: "currency", currency: "USD" }}
aria-label="Price"
/>Props
Passes through to Morphos's NumberField — value/defaultValue, min/max/step, disabled,
required, name, formatOptions (an Intl.NumberFormat options object, e.g. for currency
display), onValueChange.
The underlying input is type="text" with inputmode="decimal" — not type="number" — so no
browser-native spinner UI ever shows through; the decrement/increment buttons are the only way to
step the value with a pointer.