All components outlinesubtleflushedxssmmdlg
Number Input
Numeric fields with increment and decrement steppers in outline, subtle, and flushed variants.
1234567891011121314151617import { NumberInput } from "#components/ui/base-ui/number-input.tsx";import { accentPalette } from "@stylexblocks/ui/themes/palettes.ts";
export function Example() { return ( // Base UI's NumberField needs a Group around the input and steppers; // Control renders Increment/Decrement with chevron icons by default <NumberInput.Root defaultValue={4} min={0} variant="outline" size="md" palette={accentPalette}> <NumberInput.Group> <NumberInput.Input aria-label="Quantity" /> <NumberInput.Control /> </NumberInput.Group> </NumberInput.Root> );}
// controlled: <NumberInput.Root value={value} onValueChange={setValue}>Variants
Sizes
Install
1npx stylexblocks add number-inputSource
components/ui/base-ui/number-input.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427import * as stylex from "@stylexjs/stylex";import { NumberField as BaseNumberField } from "@base-ui-components/react/number-field";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles } from "#lib/recipe-props.ts";import { accentPalette } from "@stylexblocks/ui/themes/palettes.ts";import { numberInputIncrementTriggerIconVars, numberInputDecrementTriggerIconVars,} from '@stylexblocks/ui/slot-recipes/number-input.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const numberInputRoot = stylex.create({ base: { position: 'relative', zIndex: 0, isolation: 'isolate', width: sizes.full, },})
const numberInputInput = stylex.create({ base: { width: '100%', minWidth: 'var(--input-height)', outline: 'none', position: 'relative', appearance: 'none', textAlign: 'start', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, height: 'var(--input-height)', '--focus-color': colorPalette.focusRing, '--error-color': semanticColors.borderError, '--focus-ring-color': { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, borderColor: { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, verticalAlign: 'top', paddingInlineEnd: 'calc(var(--stepper-width) + 0.5rem)', },})
const numberInputInputSizes = stylex.create({ xs: { borderRadius: semanticRadii.control, '--stepper-width': sizes._4, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, paddingInline: spacing._2, '--input-height': sizes.controlXs, }, sm: { borderRadius: semanticRadii.control, '--stepper-width': sizes._5, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, paddingInline: spacing._2_5, '--input-height': sizes.controlSm, }, md: { borderRadius: semanticRadii.control, '--stepper-width': sizes._6, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, paddingInline: spacing._3, '--input-height': sizes.controlMd, }, lg: { borderRadius: semanticRadii.control, '--stepper-width': sizes._6, fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingInline: spacing._4_5, '--input-height': sizes.controlLg, },})
const numberInputInputVariants = stylex.create({ outline: { backgroundColor: semanticColors.bg, borderWidth: '1px', borderColor: { default: semanticColors.border, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', ':hover': semanticColors.borderEmphasized, }, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingWidth: 0, }, subtle: { borderWidth: '1px', borderColor: { default: 'transparent', ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, backgroundColor: semanticColors.bgMuted, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, }, flushed: { backgroundColor: 'transparent', borderBottomWidth: '1px', borderBottomColor: semanticColors.border, borderRadius: 0, paddingInline: 0, borderColor: { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', ':focus-visible': 'var(--focus-color)', }, boxShadow: { default: null, ':focus-visible': '0px 1px 0px 0px var(--focus-color)', }, },})
const numberInputControl = stylex.create({ base: { display: 'flex', flexDirection: 'column', position: 'absolute', top: 0, insetInlineEnd: '0px', margin: '1px', width: 'var(--stepper-width)', height: 'calc(100% - 2px)', zIndex: 1, borderInlineStartWidth: '1px', },})
const numberInputControlSizes = stylex.create({ xs: { fontSize: fontSizes._2xs, '--stepper-width': sizes._4, }, sm: { fontSize: fontSizes.xs, '--stepper-width': sizes._5, }, md: { fontSize: fontSizes.sm, '--stepper-width': sizes._6, }, lg: { fontSize: fontSizes.sm, '--stepper-width': sizes._6, },})
const numberInputIncrementTrigger = stylex.create({ base: { display: 'flex', justifyContent: 'center', alignItems: 'center', flex: 1, userSelect: 'none', cursor: cursor.button, lineHeight: 1, color: semanticColors.fgMuted, '--stepper-base-radius': radii.xs, '--stepper-radius': 'calc(var(--stepper-base-radius) + 1px)', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, backgroundColor: { default: null, ':hover': semanticColors.bgMuted, ':active': semanticColors.bgEmphasized, }, borderTopEndRadius: 'var(--stepper-radius)', [numberInputIncrementTriggerIconVars.width]: '1em', [numberInputIncrementTriggerIconVars.height]: '1em', },})
const numberInputIncrementTriggerIcon = stylex.create({ base: { width: numberInputIncrementTriggerIconVars.width, height: numberInputIncrementTriggerIconVars.height, },})
const numberInputDecrementTrigger = stylex.create({ base: { display: 'flex', justifyContent: 'center', alignItems: 'center', flex: 1, userSelect: 'none', cursor: cursor.button, lineHeight: 1, color: semanticColors.fgMuted, '--stepper-base-radius': radii.xs, '--stepper-radius': 'calc(var(--stepper-base-radius) + 1px)', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, backgroundColor: { default: null, ':hover': semanticColors.bgMuted, ':active': semanticColors.bgEmphasized, }, borderTopWidth: '1px', borderBottomEndRadius: 'var(--stepper-radius)', [numberInputDecrementTriggerIconVars.width]: '1em', [numberInputDecrementTriggerIconVars.height]: '1em', },})
const numberInputDecrementTriggerIcon = stylex.create({ base: { width: numberInputDecrementTriggerIconVars.width, height: numberInputDecrementTriggerIconVars.height, },})
const numberInputValueText = stylex.create({ base: { fontWeight: fontWeights.medium, fontFeatureSettings: 'pnum', fontVariantNumeric: 'proportional-nums', },})
export type NumberInputSize = keyof typeof numberInputInputSizes
export type NumberInputVariant = keyof typeof numberInputInputVariants
const numberInputRecipe = defineSlotRecipe({ slots: { root: { styles: numberInputRoot }, input: { styles: numberInputInput, sizes: numberInputInputSizes, variants: numberInputInputVariants, }, control: { styles: numberInputControl, sizes: numberInputControlSizes }, incrementTrigger: { styles: numberInputIncrementTrigger }, decrementTrigger: { styles: numberInputDecrementTrigger }, }, defaultVariants: { size: "md", variant: "outline" },});
const chrome = stylex.create({ btnReset: { backgroundColor: "transparent", borderWidth: 0, padding: 0, color: "inherit", fontFamily: "inherit", cursor: "pointer", },});
function ChevronUpIcon() { return ( <svg viewBox="0 0 12 12" fill="none" aria-hidden="true" {...stylex.props(numberInputIncrementTriggerIcon.base)} > <path d="M3 7.5 6 4.5l3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
function ChevronDownIcon() { return ( <svg viewBox="0 0 12 12" fill="none" aria-hidden="true" {...stylex.props(numberInputDecrementTriggerIcon.base)} > <path d="m3 4.5 3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
function ControlNode({ children, ...props }: React.ComponentProps<"div">) { return ( <div {...props}> {children ?? ( <> <Increment /> <Decrement /> </> )} </div> );}
function IncrementNode({ children, ...props}: React.ComponentProps<typeof BaseNumberField.Increment>) { return ( <BaseNumberField.Increment aria-label="Increment" {...props}> {children ?? <ChevronUpIcon />} </BaseNumberField.Increment> );}
function DecrementNode({ children, ...props}: React.ComponentProps<typeof BaseNumberField.Decrement>) { return ( <BaseNumberField.Decrement aria-label="Decrement" {...props}> {children ?? <ChevronDownIcon />} </BaseNumberField.Decrement> );}
const { withProvider, withContext } = createSlotRecipeContext(numberInputRecipe);
const NumberInputRoot = withProvider(BaseNumberField.Root, "root", "NumberInputRoot");
function Root({ palette = accentPalette, ...props}: React.ComponentProps<typeof NumberInputRoot>) { return <NumberInputRoot palette={palette} {...props} />;}Root.displayName = "NumberInputRoot";
const Increment = withContext( IncrementNode, (v) => [chrome.btnReset, ...resolveSlotStyles(numberInputRecipe.slots.incrementTrigger, v)], "NumberInputIncrement",);
const Decrement = withContext( DecrementNode, (v) => [chrome.btnReset, ...resolveSlotStyles(numberInputRecipe.slots.decrementTrigger, v)], "NumberInputDecrement",);
/** * Number Input — Base UI's NumberField wearing the number-input slot recipe. * `<NumberInput.Root variant size>` resolves the recipe once and every part * inherits it through context. Increment and Decrement ship default chevron * icons. */export const NumberInput = { Root, Group: withContext(BaseNumberField.Group, () => [], "NumberInputGroup"), Input: withContext(BaseNumberField.Input, "input", "NumberInputInput"), Control: withContext(ControlNode, "control", "NumberInputControl"), Increment, Decrement,};
export type NumberInputRootProps = React.ComponentProps<typeof NumberInput.Root>;export type NumberInputGroupProps = React.ComponentProps<typeof NumberInput.Group>;export type NumberInputInputProps = React.ComponentProps<typeof NumberInput.Input>;export type NumberInputControlProps = React.ComponentProps<typeof NumberInput.Control>;export type NumberInputIncrementProps = React.ComponentProps<typeof NumberInput.Increment>;export type NumberInputDecrementProps = React.ComponentProps<typeof NumberInput.Decrement>;