# Numeric and specialized input

Number fields, sliders, one-time codes and fractional ratings.

## Quick start

After the [application setup](/docs/installation/#component-consumption), copy this component into your application. It imports only public packages; the complete live demo later on this page also uses documentation layout helpers.

```tsx
'use client';
import { useId } from 'react';
import { Plus, Minus } from '@lucide/icons';
import { Icon } from '@fayegram-ai/faye-ui/core/icon';
import { NumberField } from '@fayegram-ai/faye-ui/controls/number-field';
import { Rating } from '@fayegram-ai/faye-ui/controls/rating';
import { Stack } from '@fayegram-ai/faye-ui/layout/layout';

export function TripNumbers() {
  const id = useId();
  return <Stack gap="4">
    <label htmlFor={id}>Number of people</label>
    <NumberField.Root id={id} name="people" locale="en-US" min={1} max={10} defaultValue={2}>
      <NumberField.Group>
        <NumberField.Decrement aria-label="One fewer person"><Icon icon={Minus} /></NumberField.Decrement>
        <NumberField.Input />
        <NumberField.Increment aria-label="One more person"><Icon icon={Plus} /></NumberField.Increment>
      </NumberField.Group>
    </NumberField.Root>
    <Rating name="rating" label="How was your day?" defaultValue={3.5} precision={0.5} />
  </Stack>;
}
```

API reference: [Icon](/docs/api-core-icon/), [Number Field](/docs/api-controls-number-field/), [Rating](/docs/api-controls-rating/).

## Usage contract
NumberField uses an explicit locale. Range thumbs need distinct names. OTPField requires a declared slot length for stable markup. Rating supports fractional precision and an optional clear action; its native radio group is bounded to at most 100 choices.

## Customization
Use semantic tokens for visual changes, public component parts for structural customization, and explicit props for instance behavior. The surrounding site lets you compare light and dark, different brands and density.

## Source and verification
The examples below import actual public package entries. Full source is downloadable. Source coverage does not establish keyboard, hydration or browser compatibility; those require the application test suite.

## Rating previews

Pointer hover previews a precision step without changing the selected or submitted native radio value. `onPreviewChange` receives the preview value, then null on pointer exit, for an optional visible hint. Keyboard selection and `onValueChange` still own the committed value. Disabled and read-only ratings do not preview; touch activation remains a direct selection.

Fractional pointer targets follow the centered star artwork, including logical direction. Keyboard focus surrounds the entire active star, rather than a narrow fractional slice; keyboard selection clears any stale pointer preview. NumberField gives its input one group frame on keyboard focus, while increment/decrement controls use their own inset frame.

## Rating appearance and preview

Empty rating values use outlined stars by default; selected values fill the artwork. Hover previews highlight the values that will be selected without changing form submission, and reset on pointer leave or keyboard interaction. Keyboard focus marks one complete item with a compact inset frame.

Pass `icon` and `emptyIcon` to use matching custom artwork, such as the heart example. Both are decorative React nodes; accessible labels, fractional hit regions, native radio keyboard behavior, RTL and form values remain owned by Rating. Use matching icon dimensions; the visual frame follows `--faye-ui-size-icon-lg`. Theme accent tokens control filled and preview colors.

## Focus, narrow layouts and scrubbing

NumberField fits the available inline width. Its input can shrink while the increment and decrement controls retain their targets, including with enlarged text.

Slider keyboard focus uses one inset ring on the visible thumb; its hidden native input does not draw a second outline. Hover and drag feedback animate without changing the track geometry, and system/provider reduced motion removes those transitions. On coarse pointers the horizontal control is at least 44px high and the vertical control is at least 44px wide.

Vertical sliders keep their value readout, track, endpoint thumbs and marks inside the root height. The track uses the remaining height after the readout and spacing; set the root block size when a longer track is needed. Mark dots align with the track in LTR and RTL. The vertical examples include both a readout and following content to make their layout visible.

 NumberField actions at their limits use disabled styling and do not highlight as available actions.

Read-only NumberField values remain focusable and readable. Increment/decrement parts expose unavailable actions through `aria-disabled`, including custom-rendered parts, and suppress hover/press styling. Scrub areas use a neutral cursor when read-only and an unavailable cursor when disabled; neither state changes the value through dragging.

OTP inputs shrink within their containing block, including the grouped-code example with `OTPField.Separator`. Coarse-pointer inputs retain readable touch typography. `NumberField.ScrubArea` offers pointer dragging on the label while the input and increment/decrement buttons remain available alternatives. Its optional custom cursor depends on browser Pointer Lock support; Safari intentionally omits that cursor.

An uncontrolled `Rating` follows its current native form owner when reset, including external forms mounted later or replaced under the same ID. Canceled resets preserve its selection. Controlled values remain owned by the application. Read-only ratings keep the selected artwork and form value; combining `readOnly` with `disabled` also applies the disabled appearance and omits the value from submission.

## Interaction variants and track feedback

Enabled sliders use `interaction={{ variant: 'minimal', level: 'standard' }}` by default. The rail and selected fill become slightly thicker on hover, dragging or keyboard-visible focus. A horizontal rail grows taller; a vertical rail grows wider. Expansion stays centered while thumb size, value positions, marks and layout stay stable. Feedback continues while dragging outside the control. Disabled controls and individually disabled thumbs remain idle.

Interaction appearance has three independent choices:

| Setting | Choices | Purpose |
| --- | --- | --- |
| `variant` | `classic`, `minimal`, `soft` | Selects a complete feedback preset. Classic retains existing thumb feedback; minimal expands the rail; soft adds a gentle tonal underlay. |
| `level` | `standard`, `enhanced` | Chooses the strength of the selected preset. Default track expansion is 1.3× or 1.5×. |
| `effects.track.type` | `expand`, `highlight`, `none` | Overrides the track behavior. Highlight adds tonal feedback without expansion; none suppresses optional track decoration. |

An `expand` effect accepts a finite `scale` from 1 to 2. Expand and highlight accept an optional nonnegative `duration` in milliseconds. Without an override, transitions use the fast-motion token. System and scoped reduced motion always remove animation, including when a custom duration is supplied. The existing single inset keyboard-focus indicator remains available for every variant.

```tsx
import { FayeUIProvider } from '@fayegram-ai/faye-ui/provider/provider';
import { Slider } from '@fayegram-ai/faye-ui/controls/slider';

export function ListeningVolume() {
  return <FayeUIProvider interaction={{ variant: 'soft', level: 'standard' }}>
    <Slider.Root defaultValue={40} interaction={{
      effects: { track: { type: 'expand', scale: 1.4, duration: 170 } }
    }}>
      <Slider.Control><Slider.Track><Slider.Indicator />
        <Slider.Thumb aria-label="Listening volume" />
      </Slider.Track></Slider.Control>
    </Slider.Root>
  </FayeUIProvider>;
}
```

The live comparison demonstrates all three variants, both levels, horizontal and vertical sliders, explicit effects and disabled controls. The same options work on `Slider.DiscreteRoot` and `ScrollArea.Root`. These visual settings do not change values, steps, form submission or keyboard behavior.

In forced-color mode, the selected range and remaining rail use distinct system colors. Thumbs retain a single visible keyboard-focus indicator, and disabled sliders use the system unavailable color without stacking translucent layers.

## Executable example source

```tsx
'use client';
import { Plus, Minus, Heart, MoveHorizontal } from '@lucide/icons';
import { Icon } from '@fayegram-ai/faye-ui/core/icon';
import { useId, useState } from 'react';
import { NumberField } from '@fayegram-ai/faye-ui/controls/number-field';
import { Slider } from '@fayegram-ai/faye-ui/controls/slider';
import { OTPField } from '@fayegram-ai/faye-ui/controls/otp-field';
import { Rating } from '@fayegram-ai/faye-ui/controls/rating';
import { Inline, Stack } from '@fayegram-ai/faye-ui/layout/layout';
import { Text } from '@fayegram-ai/faye-ui/core/text';
import InteractionVariants from './interaction-variants';
import { Section, Result } from '../example';
export default function Numeric() {
    const id = useId(), [rating, setRating] = useState(3.5), [code, setCode] = useState(''), [preview, setPreview] = useState<number | null>(null);
    return <><InteractionVariants/><Section title="Locale-aware number entry"><NumberField.Root id={`${id}-seats`} min={1} max={50} defaultValue={5}><NumberField.ScrubArea><label htmlFor={`${id}-seats`}>Seats</label><NumberField.ScrubAreaCursor><Icon icon={MoveHorizontal} size="sm"/></NumberField.ScrubAreaCursor></NumberField.ScrubArea><NumberField.Group><NumberField.Decrement aria-label="Remove a seat"><Icon icon={Minus} size="sm"/></NumberField.Decrement><NumberField.Input /><NumberField.Increment aria-label="Add a seat"><Icon icon={Plus} size="sm"/></NumberField.Increment></NumberField.Group></NumberField.Root></Section><Section title="Continuous and range sliders"><Slider.Root defaultValue={40} min={0} max={100}><Slider.Value /><Slider.Control><Slider.Track><Slider.Indicator /><Slider.Thumb aria-label="Volume"/></Slider.Track></Slider.Control></Slider.Root><Slider.Root defaultValue={[20, 70]} min={0} max={100}><Slider.Control><Slider.Track><Slider.Indicator /><Slider.Thumb index={0} aria-label="Minimum threshold"/><Slider.Thumb index={1} aria-label="Maximum threshold"/></Slider.Track></Slider.Control></Slider.Root></Section><Section title="Vertical sliders"><Inline gap="8" align="start" wrap>{([false,true] as const).map(marked=><Stack key={String(marked)} gap="3"><Text as="span" size="sm">{marked?'Marked level':'Listening level'}</Text><Slider.Root orientation="vertical" defaultValue={50} formatValue={value=>`${value}%`}><Slider.Value/><Slider.Control><Slider.Track><Slider.Indicator/></Slider.Track><Slider.Thumb aria-label={marked?'Marked vertical level':'Vertical listening level'}/>{marked&&<Slider.Marks marks={[{value:0,label:'0%'},{value:50,label:'50%'},{value:100,label:'100%'}]}/>}</Slider.Control></Slider.Root><Text as="span" size="sm" tone="muted">0–100%</Text></Stack>)}</Inline></Section><Section title="One-time code"><label htmlFor={`${id}-otp`}>Verification code</label><OTPField.Root id={`${id}-otp`} length={6} value={code} onValueChange={setCode}>{Array.from({ length: 6 }, (_, i) => <OTPField.Input key={i} aria-label={i === 0 ? undefined : `Character ${i + 1} of 6`}/>)}</OTPField.Root><Result>Entered {code.length} of 6 characters. This example sends nothing.</Result></Section><Section title="Grouped one-time code"><label htmlFor={`${id}-grouped-otp`}>Grouped verification code</label><OTPField.Root id={`${id}-grouped-otp`} length={6}>{Array.from({length:3},(_,i)=><OTPField.Input key={i} aria-label={i===0?undefined:`Grouped character ${i+1} of 6`}/>)}<OTPField.Separator aria-hidden="true">–</OTPField.Separator>{Array.from({length:3},(_,i)=><OTPField.Input key={i+3} aria-label={`Grouped character ${i+4} of 6`}/>)}</OTPField.Root></Section><Section title="Fractional, clearable rating"><Rating label="Clarity" value={rating} onValueChange={setRating} onPreviewChange={setPreview} precision={.5}/><Rating label="Read-only score" value={4.5} precision={.5} readOnly/><Rating label="How much did you enjoy it?" defaultValue={2} clearable={false} icon={<Icon icon={Heart} size="lg" fill="currentColor"/>} emptyIcon={<Icon icon={Heart} size="lg"/>}/><Result>Rating: {rating}{preview === null ? '' : ` · Preview: ${preview}`}</Result></Section></>;
}
```
