Pallas UI
DocsComponents
Core Concepts
    • Introduction
    • Getting Started
    • Theming
    • Color Tokens
    • Spacing & Sizing
    • Layout Guide
    • AspectRatio
    • Box
    • Flex
    • Grid
    • Shapes
Previews
    • Accordion
    • Alert
    • Avatar
    • Badge
    • Breadcrumb
    • Button
    • Carousel
    • Checkbox
    • Combobox
    • Command
    • Date Picker
    • Form
    • Input
    • Input OTP
    • Label
    • MenuBar
    • Modal
    • Popover
    • Progress
    • Radio Group
    • Segmented
    • Select
    • Separator
    • Sheet
    • Sidebar
    • Skeleton
    • Slider
    • Spinner
    • Steps
    • Switch
    • Tabs
    • Textarea
    • Toast
    • Tooltip
    • Typography
  1. Components
  2. Input

Input

Displays a form input field or a component that looks like an input field.

Installation

Copy and paste the following code into your project

import { Slot } from '@radix-ui/react-slot'
import { css, cx } from '@styled-system/css'
import { type InputVariantProps, icon, input } from '@styled-system/recipes'
import { format } from 'date-fns'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { Calendar } from 'lucide-react'
import React from 'react'
import { DayPicker } from '~/ui/daypicker'
import Popover from '~/ui/popover'
 
const InputContext = React.createContext<
  | ({
      id: string
      dataStatus?: 'error' | 'success' | 'warning'
    } & InputVariantProps)
  | null
>(null)
 
// Hook to ensure components are used within InputRoot
const useInputContext = () => {
  const context = React.useContext(InputContext)
  if (!context) {
    throw new Error('Input components must be used within an Input component')
  }
  return context
}
 
// Root component
const InputRoot = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> &
    InputVariantProps & { 'data-status'?: 'error' | 'success' | 'warning' }
>(({ className, styling, size, radii, 'data-status': dataStatus, ...props }, ref) => {
  const id = React.useId()
  const { root } = input({ styling, size, radii })
  return (
    <InputContext.Provider value={{ id, dataStatus, styling, size, radii }}>
      <div ref={ref} className={cx(root, className)} {...props} />
    </InputContext.Provider>
  )
})
InputRoot.displayName = 'Input'
 
// Prefix component
const InputPrefix = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => {
    const { size, radii } = useInputContext()
    const { prefix } = input({ size, radii })
    return <div ref={ref} className={cx(prefix, className)} {...props} />
  },
)
InputPrefix.displayName = 'Input.Prefix'
 
// Postfix component
const InputPostfix = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  ({ className, ...props }, ref) => {
    const { size, radii } = useInputContext()
    const { postfix } = input({ size, radii })
    return <div ref={ref} className={cx(postfix, className)} {...props} />
  },
)
InputPostfix.displayName = 'Input.Postfix'
 
type InputTextProps = React.InputHTMLAttributes<HTMLInputElement> & {
  formatter?: (value: string) => string
  maxLength?: number
  showCount?: boolean
  status?: 'error' | 'success' | 'warning'
}
 
// Text input component
const InputText = React.forwardRef<HTMLInputElement, InputTextProps>(
  (
    { className, formatter, maxLength, showCount, status, onChange, value, defaultValue, ...props },
    ref,
  ) => {
    const { id, dataStatus, styling, size, radii } = useInputContext()
    const { field, charCount } = input({ styling, size, radii })
    const [inputValue, setInputValue] = React.useState(value || defaultValue || '')
    const characterCount = String(inputValue).length
 
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      let newValue = e.target.value
      if (formatter) {
        newValue = formatter(newValue)
      }
      if (maxLength) {
        newValue = newValue.slice(0, maxLength)
      }
      setInputValue(newValue)
      onChange?.(e)
    }
 
    return (
      <>
        <Slot className={css({ flexGrow: 1 })}>
          <input
            id={id}
            ref={ref}
            type="text"
            value={value ?? inputValue}
            onChange={handleChange}
            maxLength={maxLength}
            className={cx(field, className)}
            data-status={status || dataStatus}
            data-char-count={showCount}
            {...props}
          />
        </Slot>
        {showCount && maxLength && (
          <div className={charCount}>
            {characterCount}/{maxLength}
          </div>
        )}
      </>
    )
  },
)
InputText.displayName = 'Input.Text'
 
type InputNumberProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> & {
  controls?: boolean
  step?: number
  min?: number
  max?: number
}
 
const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>(
  ({ className, controls = true, step = 1, min, max, value, onChange, ...props }, ref) => {
    const { id, dataStatus, styling, size, radii } = useInputContext()
    const { field, control } = input({ styling, size, radii })
    const { disabled } = props
    const [localValue, setLocalValue] = React.useState<number | undefined>(
      value !== undefined ? Number(value) : undefined,
    )
 
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      const newValue = e.target.value === '' ? undefined : Number(e.target.value)
      setLocalValue(newValue)
      onChange?.(e)
    }
 
    const increment = () => {
      if (localValue === undefined) {
        const newValue = min ?? 0
        setLocalValue(newValue)
        simulateInputChange(newValue)
      } else {
        const newValue = Math.min(max ?? Number.POSITIVE_INFINITY, localValue + step)
        setLocalValue(newValue)
        simulateInputChange(newValue)
      }
    }
 
    const decrement = () => {
      if (localValue === undefined) {
        const newValue = min ?? 0
        setLocalValue(newValue)
        simulateInputChange(newValue)
      } else {
        const newValue = Math.max(min ?? Number.NEGATIVE_INFINITY, localValue - step)
        setLocalValue(newValue)
        simulateInputChange(newValue)
      }
    }
 
    // Helper function to simulate input change event
    const simulateInputChange = (newValue: number) => {
      const event = {
        target: { value: String(newValue) },
      } as React.ChangeEvent<HTMLInputElement>
      onChange?.(event)
    }
 
    return (
      <div
        className={css({ display: 'flex', alignItems: 'center', width: '100%', height: '100%' })}
      >
        <Slot className={css({ flexGrow: 1, position: 'relative' })}>
          <input
            id={id}
            ref={ref}
            type="number"
            value={value ?? localValue ?? ''}
            onChange={handleChange}
            min={min}
            max={max}
            step={step}
            data-status={dataStatus}
            className={cx(
              field,
              css({
                // Hide browser default spinners
                WebkitAppearance: 'textfield',
                appearance: 'textfield',
              }),
              className,
            )}
            {...props}
          />
        </Slot>
        {controls && (
          <div
            className={css({
              display: 'flex',
              flexDirection: 'column',
              justifyContent: 'space-between',
              height: '100%',
            })}
          >
            <button
              type="button"
              onClick={increment}
              disabled={
                disabled ||
                (localValue !== undefined && localValue >= (max ?? Number.POSITIVE_INFINITY))
              }
              className={control}
            >
              <ChevronUp size={14} />
            </button>
            <button
              type="button"
              onClick={decrement}
              disabled={
                disabled ||
                (localValue !== undefined && localValue <= (min ?? Number.NEGATIVE_INFINITY))
              }
              className={control}
            >
              <ChevronDown size={14} />
            </button>
          </div>
        )}
      </div>
    )
  },
)
InputNumber.displayName = 'Input.Number'
 
type InputDayPickerProps = Omit<
  React.InputHTMLAttributes<HTMLInputElement>,
  'value' | 'onChange'
> & {
  value?: Date
  onChange?: (date: Date | undefined) => void
  format?: string
  placeholder?: string
}
 
const InputDayPicker = React.forwardRef<HTMLInputElement, InputDayPickerProps>(
  (
    { className, value, onChange, format: formatStr = 'PP', placeholder = 'Pick a date', ...props },
    ref,
  ) => {
    const { id, dataStatus, styling, size, radii } = useInputContext()
    const { field, postfix } = input({ styling, size, radii })
    const [selected, setSelected] = React.useState<Date | undefined>(value)
 
    // Update internal state when value prop changes
    React.useEffect(() => {
      setSelected(value)
    }, [value])
 
    const handleSelect = (date: Date | undefined) => {
      setSelected(date)
      onChange?.(date)
    }
 
    return (
      <Popover.Root>
        <Popover.Trigger className={css({ display: 'flex', width: '100%' })}>
          <div className={css({ position: 'relative', width: '100%', display: 'flex' })}>
            <Slot className={css({ flexGrow: 1 })}>
              <input
                id={id}
                ref={ref}
                type="text"
                readOnly
                value={selected ? format(selected, formatStr) : ''}
                placeholder={placeholder}
                data-status={dataStatus}
                className={cx(field, className)}
                {...props}
              />
            </Slot>
            <div className={postfix}>
              <Calendar className={icon()} />
            </div>
          </div>
        </Popover.Trigger>
        <Popover.Content>
          <DayPicker mode="single" selected={selected} onSelect={handleSelect} />
        </Popover.Content>
      </Popover.Root>
    )
  },
)
 
InputDayPicker.displayName = 'Input.DayPicker'
 
// Update the Input export
export const Input = Object.assign(InputRoot, {
  Prefix: InputPrefix,
  Postfix: InputPostfix,
  Text: InputText,
  Number: InputNumber,
  DayPicker: InputDayPicker,
})

Update the import paths to match your project setup

Usage

import { Input } from '@/components/ui/input'
// Basic text input
<Input>
  <Input.Text placeholder="Email" />
</Input>
 
// Number input with controls
<Input>
  <Input.Number min={0} max={100} />
</Input>
 
// Date picker input
<Input>
  <Input.DayPicker placeholder="Select a date" />
</Input>
 
// Input with prefix/postfix
<Input>
  <Input.Prefix>https://</Input.Prefix>
  <Input.Text placeholder="Website" />
  <Input.Postfix>.com</Input.Postfix>
</Input>

Examples

Default

Variants

Size Variants

Radii Variants

Status Variants

With Prefix/Postfix

https://
.com

Number Input

Date Picker

File Input

Disabled

With Label

With Button

Form

Character Count

0/50

Built with ❤️ by the carbonteq team. The source code is available on GitHub.

© 2025 Pallas UI. All rights reserved.