← Dashboard
Zubeyr··Componentsinputsforms

Input

A quiet dashboard input with optional leading and trailing slots, plus a Field wrapper for label, hint and error.

Live demo ↗

Installation

Run the shadcn CLI in a project that already has components.json. Using the @tomei tab? Register the namespace once first.

pnpm dlx shadcn@latest add https://tomei.zubeyr.dev/ui/r/input.json
  • →Files land in components/dashboard.

Usage

example.tsx
import { Mail, Search } from 'lucide-react'

import { Field, Input } from '@/components/dashboard/input'

export default function Demo() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-5">
      <Input
        placeholder="Search"
        leading={<Search />}
        trailing={<kbd className="font-sans text-xs">⌘K</kbd>}
      />
      <Field label="Email" htmlFor="email" hint="We'll only use it for receipts." required>
        <Input id="email" type="email" placeholder="you@company.com" leading={<Mail />} />
      </Field>
      <Field label="Store name" htmlFor="store" error="Store name is already taken.">
        <Input id="store" defaultValue="Tomei" aria-invalid />
      </Field>
    </div>
  )
}

Source code

input.tsx
import { cn } from '@/lib/utils'

type InputProps = React.ComponentProps<'input'> & {
  leading?: React.ReactNode
  trailing?: React.ReactNode
}

export function Input({ leading, trailing, className, ...props }: InputProps) {
  return (
    <div
      className={cn(
        'flex h-8 w-full min-w-0 items-center gap-2 rounded-lg border border-input bg-card px-2.5 text-sm text-foreground transition-[border-color] duration-150 focus-within:border-ring has-disabled:opacity-50 has-aria-invalid:border-destructive [&_svg]:size-3.5 [&_svg]:shrink-0',
        className,
      )}
    >
      {leading && <span className="flex text-muted-foreground">{leading}</span>}
      <input
        className="h-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
        {...props}
      />
      {trailing && <span className="flex text-muted-foreground">{trailing}</span>}
    </div>
  )
}

type FieldProps = {
  label: React.ReactNode
  htmlFor: string
  hint?: React.ReactNode
  error?: string
  required?: boolean
  className?: string
  children: React.ReactNode
}

export function Field({ label, htmlFor, hint, error, required, className, children }: FieldProps) {
  return (
    <div className={cn('flex flex-col gap-1.5', className)}>
      <label htmlFor={htmlFor} className="text-sm font-medium text-foreground">
        {label}
        {required && <span className="text-muted-foreground"> *</span>}
      </label>
      {children}
      {error ? (
        <span id={`${htmlFor}-error`} role="alert" className="text-xs text-destructive">
          {error}
        </span>
      ) : (
        hint && (
          <span id={`${htmlFor}-hint`} className="text-xs text-muted-foreground">
            {hint}
          </span>
        )
      )}
    </div>
  )
}