Number Field

A numeric input with increment and decrement controls.

View source
import * as NumberField from "@danielfrg/solid-ui/number-field"
import styles from "./index.module.css"

export function DemoNumberFieldHero() {
  return (
    <NumberField.Root class={styles.root} defaultValue={50} minValue={0} maxValue={100}>
      <NumberField.Label class={styles.label}>Quantity</NumberField.Label>
      <div class={styles.group}>
        <NumberField.DecrementTrigger class={styles.button} aria-label="Decrement">
          <MinusIcon />
        </NumberField.DecrementTrigger>
        <NumberField.Input class={styles.input} />
        <NumberField.IncrementTrigger class={styles.button} aria-label="Increment">
          <PlusIcon />
        </NumberField.IncrementTrigger>
      </div>
      <NumberField.HiddenInput />
    </NumberField.Root>
  )
}

function MinusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}

function PlusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M7 3V11M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}
.root {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  width: 10rem;
}

.label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  color: var(--color-gray-900);
}

.group {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-gray-300);
  border-radius: 6px;
  overflow: hidden;
}

.input {
  all: unset;
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  padding: 6px 0;
  color: var(--color-gray-900);
  min-width: 0;
}

.button {
  all: unset;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  color: var(--color-gray-600);
  cursor: pointer;
  flex-shrink: 0;
}

.button:hover {
  background-color: var(--color-gray-100);
  color: var(--color-gray-900);
}

.button[data-disabled] {
  opacity: 0.4;
  cursor: default;
}

.description {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.error {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: #dc2626;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Examples

Default value

Use defaultValue to set the initial value without controlling the state.

View source
import * as NumberField from "@danielfrg/solid-ui/number-field"
import styles from "./index.module.css"

export function DemoNumberFieldDefaultValue() {
  return (
    <NumberField.Root class={styles.root} defaultValue={40}>
      <NumberField.Label class={styles.label}>Quantity</NumberField.Label>
      <div class={styles.group}>
        <NumberField.DecrementTrigger class={styles.button} aria-label="Decrement">
          <MinusIcon />
        </NumberField.DecrementTrigger>
        <NumberField.Input class={styles.input} />
        <NumberField.IncrementTrigger class={styles.button} aria-label="Increment">
          <PlusIcon />
        </NumberField.IncrementTrigger>
      </div>
      <NumberField.HiddenInput />
    </NumberField.Root>
  )
}

function MinusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}

function PlusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M7 3V11M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}
.root {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  width: 10rem;
}

.label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  color: var(--color-gray-900);
}

.group {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-gray-300);
  border-radius: 6px;
  overflow: hidden;
}

.input {
  all: unset;
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  padding: 6px 0;
  color: var(--color-gray-900);
  min-width: 0;
}

.button {
  all: unset;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  color: var(--color-gray-600);
  cursor: pointer;
  flex-shrink: 0;
}

.button:hover {
  background-color: var(--color-gray-100);
  color: var(--color-gray-900);
}

.button[data-disabled] {
  opacity: 0.4;
  cursor: default;
}

.description {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.error {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: #dc2626;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Controlled

Use rawValue and onRawValueChange to control the numeric value externally.

Value: 40

View source
import { createSignal } from "solid-js"
import * as NumberField from "@danielfrg/solid-ui/number-field"
import styles from "./index.module.css"

export function DemoNumberFieldControlled() {
  const [rawValue, setRawValue] = createSignal(40)

  return (
    <div>
      <NumberField.Root class={styles.root} rawValue={rawValue()} onRawValueChange={setRawValue}>
        <NumberField.Label class={styles.label}>Quantity</NumberField.Label>
        <div class={styles.group}>
          <NumberField.DecrementTrigger class={styles.button} aria-label="Decrement">
            <MinusIcon />
          </NumberField.DecrementTrigger>
          <NumberField.Input class={styles.input} />
          <NumberField.IncrementTrigger class={styles.button} aria-label="Increment">
            <PlusIcon />
          </NumberField.IncrementTrigger>
        </div>
        <NumberField.HiddenInput />
      </NumberField.Root>
      <p class={styles.status}>Value: {rawValue()}</p>
    </div>
  )
}

function MinusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}

function PlusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M7 3V11M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}
.root {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  width: 10rem;
}

.label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  color: var(--color-gray-900);
}

.group {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-gray-300);
  border-radius: 6px;
  overflow: hidden;
}

.input {
  all: unset;
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  padding: 6px 0;
  color: var(--color-gray-900);
  min-width: 0;
}

.button {
  all: unset;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  color: var(--color-gray-600);
  cursor: pointer;
  flex-shrink: 0;
}

.button:hover {
  background-color: var(--color-gray-100);
  color: var(--color-gray-900);
}

.button[data-disabled] {
  opacity: 0.4;
  cursor: default;
}

.description {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.error {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: #dc2626;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Description

Use NumberField.Description to add a hint below the input.

Enter a quantity between 1 and 99.
View source
import * as NumberField from "@danielfrg/solid-ui/number-field"
import styles from "./index.module.css"

export function DemoNumberFieldDescription() {
  return (
    <NumberField.Root class={styles.root} defaultValue={1} minValue={1} maxValue={99}>
      <NumberField.Label class={styles.label}>Quantity</NumberField.Label>
      <div class={styles.group}>
        <NumberField.DecrementTrigger class={styles.button} aria-label="Decrement">
          <MinusIcon />
        </NumberField.DecrementTrigger>
        <NumberField.Input class={styles.input} />
        <NumberField.IncrementTrigger class={styles.button} aria-label="Increment">
          <PlusIcon />
        </NumberField.IncrementTrigger>
      </div>
      <NumberField.Description class={styles.description}>Enter a quantity between 1 and 99.</NumberField.Description>
      <NumberField.HiddenInput />
    </NumberField.Root>
  )
}

function MinusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}

function PlusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M7 3V11M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}
.root {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  width: 10rem;
}

.label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  color: var(--color-gray-900);
}

.group {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-gray-300);
  border-radius: 6px;
  overflow: hidden;
}

.input {
  all: unset;
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  padding: 6px 0;
  color: var(--color-gray-900);
  min-width: 0;
}

.button {
  all: unset;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  color: var(--color-gray-600);
  cursor: pointer;
  flex-shrink: 0;
}

.button:hover {
  background-color: var(--color-gray-100);
  color: var(--color-gray-900);
}

.button[data-disabled] {
  opacity: 0.4;
  cursor: default;
}

.description {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.error {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: #dc2626;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Error message

Use validationState and NumberField.ErrorMessage to show validation feedback.

Quantity must be exactly 40.
View source
import { createSignal } from "solid-js"
import * as NumberField from "@danielfrg/solid-ui/number-field"
import styles from "./index.module.css"

export function DemoNumberFieldErrorMessage() {
  const [rawValue, setRawValue] = createSignal(0)

  return (
    <NumberField.Root
      class={styles.root}
      rawValue={rawValue()}
      onRawValueChange={setRawValue}
      validationState={rawValue() !== 40 ? "invalid" : "valid"}
    >
      <NumberField.Label class={styles.label}>Quantity</NumberField.Label>
      <div class={styles.group}>
        <NumberField.DecrementTrigger class={styles.button} aria-label="Decrement">
          <MinusIcon />
        </NumberField.DecrementTrigger>
        <NumberField.Input class={styles.input} />
        <NumberField.IncrementTrigger class={styles.button} aria-label="Increment">
          <PlusIcon />
        </NumberField.IncrementTrigger>
      </div>
      <NumberField.ErrorMessage class={styles.error}>Quantity must be exactly 40.</NumberField.ErrorMessage>
      <NumberField.HiddenInput />
    </NumberField.Root>
  )
}

function MinusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}

function PlusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M7 3V11M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}
.root {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  width: 10rem;
}

.label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  color: var(--color-gray-900);
}

.group {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-gray-300);
  border-radius: 6px;
  overflow: hidden;
}

.input {
  all: unset;
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  padding: 6px 0;
  color: var(--color-gray-900);
  min-width: 0;
}

.button {
  all: unset;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  color: var(--color-gray-600);
  cursor: pointer;
  flex-shrink: 0;
}

.button:hover {
  background-color: var(--color-gray-100);
  color: var(--color-gray-900);
}

.button[data-disabled] {
  opacity: 0.4;
  cursor: default;
}

.description {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.error {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: #dc2626;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

HTML form

Use the name prop with NumberField.HiddenInput to include the value in a native form submission.

View source
import * as NumberField from "@danielfrg/solid-ui/number-field"
import styles from "./index.module.css"

export function DemoNumberFieldHtmlForm() {
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const data = new FormData(e.currentTarget)
        window.alert(JSON.stringify(Object.fromEntries(data), null, 2))
      }}
      class={styles.form}
    >
      <NumberField.Root class={styles.root} name="quantity" defaultValue={1} minValue={1}>
        <NumberField.Label class={styles.label}>Quantity</NumberField.Label>
        <div class={styles.group}>
          <NumberField.DecrementTrigger class={styles.button} aria-label="Decrement">
            <MinusIcon />
          </NumberField.DecrementTrigger>
          <NumberField.Input class={styles.input} />
          <NumberField.IncrementTrigger class={styles.button} aria-label="Increment">
            <PlusIcon />
          </NumberField.IncrementTrigger>
        </div>
        <NumberField.HiddenInput />
      </NumberField.Root>
      <div class={styles.actions}>
        <button type="reset" class={styles.button}>
          Reset
        </button>
        <button type="submit" class={styles.button}>
          Submit
        </button>
      </div>
    </form>
  )
}

function MinusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}

function PlusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M7 3V11M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}
.root {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  width: 10rem;
}

.label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  color: var(--color-gray-900);
}

.group {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-gray-300);
  border-radius: 6px;
  overflow: hidden;
}

.input {
  all: unset;
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  padding: 6px 0;
  color: var(--color-gray-900);
  min-width: 0;
}

.button {
  all: unset;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  color: var(--color-gray-600);
  cursor: pointer;
  flex-shrink: 0;
}

.button:hover {
  background-color: var(--color-gray-100);
  color: var(--color-gray-900);
}

.button[data-disabled] {
  opacity: 0.4;
  cursor: default;
}

.description {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.error {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: #dc2626;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Format options

Use formatOptions to format the displayed value, for example as currency.

View source
import * as NumberField from "@danielfrg/solid-ui/number-field"
import styles from "./index.module.css"

export function DemoNumberFieldFormat() {
  return (
    <NumberField.Root
      class={styles.root}
      defaultValue={4}
      minValue={0}
      formatOptions={{ style: "currency", currency: "USD" }}
    >
      <NumberField.Label class={styles.label}>Price</NumberField.Label>
      <div class={styles.group}>
        <NumberField.DecrementTrigger class={styles.button} aria-label="Decrement">
          <MinusIcon />
        </NumberField.DecrementTrigger>
        <NumberField.Input class={styles.input} />
        <NumberField.IncrementTrigger class={styles.button} aria-label="Increment">
          <PlusIcon />
        </NumberField.IncrementTrigger>
      </div>
      <NumberField.HiddenInput />
    </NumberField.Root>
  )
}

function MinusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}

function PlusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <path d="M7 3V11M3 7H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
    </svg>
  )
}
.root {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  width: 10rem;
}

.label {
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 500;
  color: var(--color-gray-900);
}

.group {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-gray-300);
  border-radius: 6px;
  overflow: hidden;
}

.input {
  all: unset;
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  padding: 6px 0;
  color: var(--color-gray-900);
  min-width: 0;
}

.button {
  all: unset;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  color: var(--color-gray-600);
  cursor: pointer;
  flex-shrink: 0;
}

.button:hover {
  background-color: var(--color-gray-100);
  color: var(--color-gray-900);
}

.button[data-disabled] {
  opacity: 0.4;
  cursor: default;
}

.description {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.error {
  font-size: 0.75rem;
  line-height: 1.25rem;
  color: #dc2626;
}

.status {
  margin: 1rem 0 0;
  font-size: 0.875rem;
  line-height: 1.25rem;
  color: var(--color-gray-600);
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.actions {
  display: flex;
  gap: 0.5rem;
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}