Kosmesis
Components

Number Field

A decrement/input/increment stepper, wrapping Morphos's NumberField primitive.

Installation

npx kosmesis add number-field
pnpm dlx kosmesis add number-field
yarn dlx kosmesis add number-field
bunx kosmesis add number-field

Install the following dependencies:

npm install @morphos/inputs
pnpm add @morphos/inputs
yarn add @morphos/inputs
bun add @morphos/inputs

Copy and paste the following code into your project.

number-field.tsx
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/css
pnpm add @morphos/inputs @praxisjs/css
yarn add @morphos/inputs @praxisjs/css
bun add @morphos/inputs @praxisjs/css

Copy and paste the following code into your project.

number-field.tsx
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.

PropTypeDefault
valuenumber— controlled value
defaultValuenumber
minnumber
maxnumber
stepnumber
disabledboolean
requiredboolean
namestring
formatOptionsIntl.NumberFormatOptions— e.g. { style: "currency", currency: "USD" }
onValueChange(value: number) => void

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.

On this page