Number Field

A number field lets users enter and adjust numeric values with optional increment and decrement controls.

Examples

Basic

PropDefaultTypeDescription
disabled-booleanWhen true, prevents the user from interacting with the Number Field.
disableWheelChange-booleanWhen true, prevents the value from changing on wheel scroll.
invertWheelChange-booleanWhen true, inverts the direction of the wheel change.
locale-stringThe locale to use for formatting numeric values
max-numberThe largest value allowed for the input.
min-numberThe smallest value allowed for the input.
modelValue-number | nullValue of the input. Can be binded as v-model.
step1numberThe amount that the input value changes with each increment or decrement "tick".
stepSnappingtruebooleanWhen false, prevents the value from snapping to the nearest increment of the step value.
Preview
Code

Variant & Color

PropDefaultTypeDescription
number-fieldoutline-primary{variant}-{color}Controls the visual style of the number field.
VariantDescription
outlineThe default variant.
solidThe solid variant.
~The unstyle or base variant
Preview
Code
Dynamic colors:
Static colors:

Size

PropDefaultTypeDescription
sizemdstringAllows you to change the size of the number-field.

🚀 Adjust number-field size freely using any size, breakpoints (e.g., sm:sm, xs:lg), or states (e.g., hover:lg, focus:3xl).

Preview
Code

Icons

PropDefaultTypeDescription
leading-stringThe icon to display before the input.
trailing-stringThe icon to display after the input.
Preview
Code

Form Field

The NNumberField component can be easily embedded within the NFormField component.

Preview
Code

Enter value between 10 and 5000.

Slots

NamePropsDescription
default-Allows advanced customization using sub-components, replacing the default number field structure.
content-Replaces the entire content container, including increment, decrement and input.
increment-Custom content for the number field increment.
decrement-Custom content for the number field decrement.

Custom Rendering

Use the default slot for complete control over the number field's structure. This lets you compose the number field using its individual sub-components (such as increment, decrement, and input elements, as listed in the Slots section), similar to approaches used in libraries like shadcn/ui.

Preview
Code

Presets

shortcuts/number-field.ts
type NumberFieldPrefix = 'number-field'

export const staticNumberField: Record<`${NumberFieldPrefix}-${string}` | NumberFieldPrefix, string> = {
  // base
  'number-field': 'grid gap-1.5',
  'number-field-content': 'relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-1.25em [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-1.25em',
  'number-field-decrement': 'grid place-items-center absolute top-1/2 -translate-y-1/2 left-0 p-0.75em disabled:cursor-not-allowed disabled:opacity-20',
  'number-field-input': 'flex w-full rounded-md bg-transparent h-2.5714285714285716em py-0.2857142857142857em text-0.875em text-center shadow-sm transition-colors placeholder:text-muted disabled:n-disabled focus-visible:outline-none',
  'number-field-increment': 'grid place-items-center absolute top-1/2 -translate-y-1/2 right-0 disabled:cursor-not-allowed disabled:opacity-20 p-0.75em',
}

export const dynamicNumberField: [RegExp, (params: RegExpExecArray) => string][] = [
  // dynamic preset
  [/^number-field-([^-]+)-([^-]+)$/, ([, v = 'outline', c = 'primary']) => `input-${v}-${c}`],
]

export const numberField = [
  ...dynamicNumberField,
  staticNumberField,
]

Props

types/number-field.ts
import type { NumberFieldDecrementProps, NumberFieldIncrementProps, NumberFieldInputProps, NumberFieldRootProps } from 'reka-ui'

import type { HTMLAttributes } from 'vue'
import type { NInputProps } from './input'

export interface NNumberFieldProps extends NumberFieldRootProps, Pick<NInputProps, 'leading' | 'trailing'>, BaseExtensions {
  /**
   * Allows you to add `UnaUI` input preset properties,
   * Think of it as a shortcut for adding options or variants to the preset if available.
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/number-field.ts
   * @example
   * numberField="solid-green"
   */
  numberField?: HTMLAttributes['class']

  // subcomponents
  _numberFieldContent?: NNumberFieldContentProps
  _numberFieldDecrement?: NNumberFieldDecrementProps
  _numberFieldIncrement?: NNumberFieldIncrementProps
  _numberFieldInput?: NNumberFieldInputProps
  /**
   * `UnaUI` preset configuration
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/number-field.ts
   */
  una?: NNumberFieldUnaProps
}

export interface NNumberFieldContentProps extends BaseExtensions {
  una?: Pick<NNumberFieldUnaProps, 'numberFieldContent'>
}

export interface NNumberFieldDecrementProps extends NumberFieldDecrementProps, BaseExtensions {
  icon?: string
  una?: Pick<NNumberFieldUnaProps, 'numberFieldDecrement'>
}

export interface NNumberFieldIncrementProps extends NumberFieldIncrementProps, BaseExtensions {
  icon?: string
  una?: Pick<NNumberFieldUnaProps, 'numberFieldIncrement'>
}

export interface NNumberFieldInputProps extends NumberFieldInputProps, BaseExtensions, Pick<NNumberFieldProps, 'numberField'> {
  una?: Pick<NNumberFieldUnaProps, 'numberFieldInput'>
}

export interface NNumberFieldUnaProps {
  numberFieldContent?: HTMLAttributes['class']
  numberFieldIncrement?: HTMLAttributes['class']
  numberFieldDecrement?: HTMLAttributes['class']
  numberFieldInput?: HTMLAttributes['class']
}

interface BaseExtensions {
  class?: HTMLAttributes['class']
  size?: HTMLAttributes['class']
}

Components

NumberField.vue
NumberFieldContent.vue
NumberFieldInput.vue
NumberFieldIncrement.vue
NumberFieldDecrement.vue
<script setup lang="ts">
import type { NumberFieldRootEmits } from 'reka-ui'
import type { NNumberFieldProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { NumberFieldRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '../../utils'
import NumberFieldContent from './NumberFieldContent.vue'

const props = withDefaults(defineProps<NNumberFieldProps>(), {
  size: 'md',
  leading: 'i-lucide-minus',
  trailing: 'i-lucide-plus',
  numberField: 'outline-primary',
})
const emits = defineEmits<NumberFieldRootEmits>()

const delegatedProps = reactiveOmit(props, 'class', 'numberField')

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
  <NumberFieldRoot
    v-bind="forwarded"
    :class="cn('number-field', props.class)"
  >
    <slot>
      <NumberFieldContent :size>
        <slot name="content">
          <NNumberFieldDecrement
            v-bind="forwarded._numberFieldDecrement"
            :icon="props.leading"
            :size
            :una
          >
            <slot name="decrement" />
          </NNumberFieldDecrement>
          <NNumberFieldInput
            v-bind="forwarded._numberFieldInput"
            :size
            :number-field
            :una
          />
          <NNumberFieldIncrement
            v-bind="forwarded._numberFieldIncrement"
            :icon="props.trailing"
            :size
            :una
          >
            <slot name="increment" />
          </NNumberFieldIncrement>
        </slot>
      </NumberFieldContent>
    </slot>
  </NumberFieldRoot>
</template>