InputsInput
Input
The single-line text input. The default form field across the site.
Sizes
Three sizes. The default is md (40 px tall).
Sizes
States
Default, focus, invalid, disabled.
States
With an icon
The icon sits inline with the input. It is decorative; the input is still announced by its label.
With icon
Anatomy
- Background:
--background(default) or--surface-elevated(subtle) - Border: 1 px
--borderat rest,--foregroundon focus,--destructivewhen invalid - Text:
--foregroundat 0.9375 rem, line-height 1.4 - Placeholder:
--muted-foregroundat the same size, normal weight - Padding: 12 px horizontal at md
- Radius:
--radius-md(4 px) - Focus ring: 1 px
--foregroundborder + 3 px inner shadow at 12% opacity
Implementation
input.tsx
import { forwardRef, type InputHTMLAttributes } from "react"
import { cn } from "@/lib/utils"
import SiteIcon, type SiteIconProps } from "@/components/site-icon"
type Size = "sm" | "md" | "lg"
type Tone = "default" | "subtle"
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
size?: Size
tone?: Tone
leadingIcon?: SiteIconProps["icon"]
trailingIcon?: SiteIconProps["icon"]
invalid?: boolean
}
const sizeClass: Record<Size, string> = {
sm: "ds-input--sm",
md: "",
lg: "ds-input--lg",
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
function Input({ size = "md", tone = "default", leadingIcon, trailingIcon, invalid, className, ...props }, ref) {
const input = (
<input
ref={ref}
className={cn("ds-input", sizeClass[size], `ds-input--${tone}`, invalid && "ds-input--invalid", className)}
aria-invalid={invalid || undefined}
{...props}
/>
)
if (!leadingIcon && !trailingIcon) return input
return (
<div className="ds-input-affix">
{leadingIcon ? <SiteIcon icon={leadingIcon} size="sm" tone="muted" /> : null}
{input}
{trailingIcon ? <SiteIcon icon={trailingIcon} size="sm" tone="muted" /> : null}
</div>
)
},
)Props
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | Height + padding |
tone | "default" | "subtle" | "default" | Background tone |
leadingIcon | LucideIcon | None | Leading icon (decorative) |
trailingIcon | LucideIcon | None | Trailing icon (decorative) |
invalid | boolean | false | Error state |
disabled | boolean | false | Disabled state |
Accessibility
- Every input is associated with a
<label>. The association is byhtmlFor+id, never by placeholder-as-label. - Invalid state uses
aria-invalidand is announced to assistive tech. - Error messages are announced via
aria-describedby. - Touch target is at least 40 × 40 px at the default size.