Number Input
A field that allows user input of numeric values.
Usage
import { NumberInput, type NumberInputProps } from '~/components/ui'
export const Demo = (props: NumberInputProps) => {
return (
<NumberInput defaultValue="3" {...props}>
Label
</NumberInput>
)
}
Installation
npx @park-ui/cli components add number-input
1
Styled Primitive
Copy the code snippet below into ~/components/ui/primitives/number-input.tsx
'use client'
import type { Assign } from '@ark-ui/react'
import { NumberInput } from '@ark-ui/react/number-input'
import { type NumberInputVariantProps, numberInput } from 'styled-system/recipes'
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from '~/lib/create-style-context'
const { withProvider, withContext } = createStyleContext(numberInput)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withProvider<
HTMLDivElement,
Assign<Assign<HTMLStyledProps<'div'>, NumberInput.RootProviderBaseProps>, NumberInputVariantProps>
>(NumberInput.RootProvider, 'root')
export type RootProps = ComponentProps<typeof Root>
export const Root = withProvider<
HTMLDivElement,
Assign<Assign<HTMLStyledProps<'div'>, NumberInput.RootBaseProps>, NumberInputVariantProps>
>(NumberInput.Root, 'root')
export const Control = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, NumberInput.ControlBaseProps>
>(NumberInput.Control, 'control')
export const DecrementTrigger = withContext<
HTMLButtonElement,
Assign<HTMLStyledProps<'button'>, NumberInput.DecrementTriggerBaseProps>
>(NumberInput.DecrementTrigger, 'decrementTrigger')
export const IncrementTrigger = withContext<
HTMLButtonElement,
Assign<HTMLStyledProps<'button'>, NumberInput.IncrementTriggerBaseProps>
>(NumberInput.IncrementTrigger, 'incrementTrigger')
export const Input = withContext<
HTMLInputElement,
Assign<HTMLStyledProps<'input'>, NumberInput.InputBaseProps>
>(NumberInput.Input, 'input')
export const Label = withContext<
HTMLLabelElement,
Assign<HTMLStyledProps<'label'>, NumberInput.LabelBaseProps>
>(NumberInput.Label, 'label')
export const Scrubber = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, NumberInput.ScrubberBaseProps>
>(NumberInput.Scrubber, 'scrubber')
export const ValueText = withContext<
HTMLSpanElement,
Assign<HTMLStyledProps<'span'>, NumberInput.ValueTextBaseProps>
// @ts-expect-error it exists
>(NumberInput.ValueText, 'valueText')
export { NumberInputContext as Context } from '@ark-ui/react/number-input'
import { type Assign, NumberInput } from '@ark-ui/solid'
import type { ComponentProps } from 'solid-js'
import { type NumberInputVariantProps, numberInput } from 'styled-system/recipes'
import type { HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from '~/lib/create-style-context'
const { withProvider, withContext } = createStyleContext(numberInput)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withProvider<
Assign<Assign<HTMLStyledProps<'div'>, NumberInput.RootProviderBaseProps>, NumberInputVariantProps>
>(NumberInput.RootProvider, 'root')
export type RootProps = ComponentProps<typeof Root>
export const Root = withProvider<
Assign<Assign<HTMLStyledProps<'div'>, NumberInput.RootBaseProps>, NumberInputVariantProps>
>(NumberInput.Root, 'root')
export const Control = withContext<Assign<HTMLStyledProps<'div'>, NumberInput.ControlBaseProps>>(
NumberInput.Control,
'control',
)
export const DecrementTrigger = withContext<
Assign<HTMLStyledProps<'button'>, NumberInput.DecrementTriggerBaseProps>
>(NumberInput.DecrementTrigger, 'decrementTrigger')
export const IncrementTrigger = withContext<
Assign<HTMLStyledProps<'button'>, NumberInput.IncrementTriggerBaseProps>
>(NumberInput.IncrementTrigger, 'incrementTrigger')
export const Input = withContext<Assign<HTMLStyledProps<'input'>, NumberInput.InputBaseProps>>(
NumberInput.Input,
'input',
)
export const Label = withContext<Assign<HTMLStyledProps<'label'>, NumberInput.LabelBaseProps>>(
NumberInput.Label,
'label',
)
export const Scrubber = withContext<Assign<HTMLStyledProps<'div'>, NumberInput.ScrubberBaseProps>>(
NumberInput.Scrubber,
'scrubber',
)
export const ValueText = withContext<Assign<HTMLStyledProps<'span'>, NumberInput.ValueTextProps>>(
NumberInput.ValueText,
// @ts-expect-error
'valueText',
)
export { NumberInputContext as Context } from '@ark-ui/solid'
No snippet found
Extend ~/components/ui/primitives/index.ts
with the following line:
export * as NumberInput from './number-input'
2
Add Composition
Copy the code snippet below into ~/components/ui/number-input.tsx
import { forwardRef } from 'react'
import { NumberInput as ArkNumberInput } from '~/components/ui/primitives'
export interface NumberInputProps extends ArkNumberInput.RootProps {}
export const NumberInput = forwardRef<HTMLDivElement, NumberInputProps>((props, ref) => {
const { children, ...rootProps } = props
return (
<ArkNumberInput.Root ref={ref} {...rootProps}>
{children && <ArkNumberInput.Label>{children}</ArkNumberInput.Label>}
<ArkNumberInput.Control>
<ArkNumberInput.Input />
<ArkNumberInput.IncrementTrigger>
<ChevronUpIcon />
</ArkNumberInput.IncrementTrigger>
<ArkNumberInput.DecrementTrigger>
<ChevronDownIcon />
</ArkNumberInput.DecrementTrigger>
</ArkNumberInput.Control>
</ArkNumberInput.Root>
)
})
NumberInput.displayName = 'NumberInput'
const ChevronUpIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>Chevron Up Icon</title>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m18 15l-6-6l-6 6"
/>
</svg>
)
const ChevronDownIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>Chevron Down Icon</title>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m6 9l6 6l6-6"
/>
</svg>
)
import { Show, children } from 'solid-js'
import { NumberInput as ArkNumberInput } from '~/components/ui/primitives'
export interface NumberInputProps extends ArkNumberInput.RootProps {}
export const NumberInput = (props: NumberInputProps) => {
const getChildren = children(() => props.children)
return (
<ArkNumberInput.Root {...props}>
<Show when={getChildren()}>
<ArkNumberInput.Label>{getChildren()}</ArkNumberInput.Label>
</Show>
<ArkNumberInput.Control>
<ArkNumberInput.Input />
<ArkNumberInput.IncrementTrigger>
<ChevronUpIcon />
</ArkNumberInput.IncrementTrigger>
<ArkNumberInput.DecrementTrigger>
<ChevronDownIcon />
</ArkNumberInput.DecrementTrigger>
</ArkNumberInput.Control>
</ArkNumberInput.Root>
)
}
const ChevronUpIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>Chevron Up Icon</title>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m18 15l-6-6l-6 6"
/>
</svg>
)
const ChevronDownIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<title>Chevron Down Icon</title>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m6 9l6 6l6-6"
/>
</svg>
)
Extend ~/components/ui/index.ts
with the following line:
export * from './primitives'
export { NumberInput, type NumberInputProps } from './number-input'
3
Integrate Recipe
If you're not using @park-ui/preset
, add the following recipe to yourpanda.config.ts
:
import { numberInputAnatomy } from '@ark-ui/anatomy'
import { defineSlotRecipe } from '@pandacss/dev'
const trigger = {
alignItems: 'center',
borderColor: 'border.default',
color: 'fg.muted',
cursor: 'pointer',
display: 'inline-flex',
justifyContent: 'center',
transitionDuration: 'normal',
transitionProperty: 'background, border-color, color, box-shadow',
transitionTimingFunction: 'default',
'& :where(svg)': {
width: '4',
height: '4',
},
_hover: {
background: 'gray.a2',
color: 'fg.default',
},
_disabled: {
color: 'fg.disabled',
cursor: 'not-allowed',
_hover: {
background: 'transparent',
color: 'fg.disabled',
},
},
}
export const numberInput = defineSlotRecipe({
className: 'numberInput',
slots: numberInputAnatomy.keys(),
base: {
root: {
colorPalette: 'accent',
display: 'flex',
flexDirection: 'column',
gap: '1.5',
},
control: {
borderColor: 'border.default',
borderRadius: 'l2',
borderWidth: '1px',
display: 'grid',
divideX: '1px',
gridTemplateColumns: '1fr 32px',
gridTemplateRows: '1fr 1fr',
overflow: 'hidden',
transitionDuration: 'normal',
transitionProperty: 'border-color, box-shadow',
transitionTimingFunction: 'default',
_focusWithin: {
borderColor: 'colorPalette.default',
boxShadow: '0 0 0 1px var(--colors-color-palette-default)',
},
},
input: {
background: 'transparent',
border: 'none',
gridRow: '2',
outline: 'none',
width: 'full',
},
label: {
color: 'fg.default',
fontWeight: 'medium',
},
decrementTrigger: { ...trigger, borderTopWidth: '1px' },
incrementTrigger: trigger,
},
defaultVariants: {
size: 'md',
},
variants: {
size: {
md: {
control: {
ps: '3',
h: '10',
minW: '10',
fontSize: 'md',
},
label: {
textStyle: 'sm',
},
},
lg: {
control: {
ps: '3.5',
h: '11',
minW: '11',
fontSize: 'md',
},
label: {
textStyle: 'sm',
},
},
xl: {
control: {
ps: '4',
h: '12',
minW: '12',
fontSize: 'lg',
},
label: {
textStyle: 'md',
},
},
},
},
})