Input

Text, textarea, dimension, and tag fields with built-in label, hint, and error treatment.

The project type will be added automatically

Unavailable.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/input.json
Usage
Each field is a single component that owns its label, hint, and error treatment. Drop in `iconLeading` / `iconTrailing` for inline glyphs.
TSImport
import {
  TextField,
  TextareaField,
  Composer,
  NumberInput,
  FileSizeInput,
  TagsInput,
  DurationInput,
  RepeatableInput,
} from "@/components/ui/input";
TSExample
import { MagnifyingGlass as PhMagnifyingGlass } from "@phosphor-icons/react/ssr";
import { TextField } from "@/components/ui/input";

const SearchIcon = (p: { className?: string }) => <PhMagnifyingGlass weight="duotone" {...p} />;

export function Example() {
  return (
    <TextField
      label="Project title"
      placeholder="Enter project title…"
      caption="The project type will be added automatically."
      iconLeading={SearchIcon}
      required
    />
  );
}
Composition
Anatomy of the input primitives.
TextField                       (label + caption + input in one component)
├── Label                       (renders if `label` is passed)
├── Caption                     (renders if `caption` is passed — under the label)
├── (iconLeading)               (lucide icon component)
├── <input>
├── (iconTrailing)              (lucide icon component)
└── error                       (rendered below the control in red)

TextareaField                   (same surface, multi-line + optional character count)
Composer                        (the composer in one line — `variant="panel"` titled panel, `"field"` bare)
├── heading / headingAside      (panel only — the title row)
├── menu | leadingContent       (docked bottom-left)
├── attachments / dictation     (docked bottom-right, beside submit)
├── advanced                    (the drawer underneath — yours to fill)
└── panelBackground / panelColor (the surface behind it all)

FileSizeInput                   (width × height + unit selector)
TagsInput                       (chip list driven by Enter / Tab / , / paste)
DurationInput                   (segmented d/h/m/s field, value in total seconds)
Text field
Compound component with label, hint, and error — ported from PRF4’s Untitled UI field.

The project type will be added automatically

Unavailable.

Input types
Every native HTML type the team regularly reaches for, using the shadcn Input primitive.

Your unique identifier

Validates format on blur.

US format auto-applies as you type.

Must be at least 8 characters

3- or 4-digit account number.

Email verification
The email `TextField` wired to a live deliverability check (rapid-email-verifier via a proxy route). Typing is debounced 500ms: a spinner shows while checking, a green ✓ when the address is valid, and a red ✗ with an error message when it can't receive mail.

We'll only use this to contact you about your project.

Number field
Numeric input with stepper buttons. Pass `min`, `max`, and `step` for range/granularity. Drives the number-field renderer in @thesqd/dynamic-forms.

Maximum 50 seats per workspace.

File size
Width × height with a unit selector (inches, cm, mm, pixels, points). Pair with a Label for document dimensions, canvas sizes, or print bleeds.

Output: {"h":null,"w":null,"unit":"in"}

Output: {"h":null,"w":null,"unit":"px"}

pixels

Click the pencil to split into editable cards; ✓ to converge.

Textareas
Multi-line text input with and without validation.

Tell us a bit about yourself.

Uses Chrome's Web Speech API — button hides in other browsers. voiceGlow blooms while it listens.

Composer
`<Composer variant="field">` — the Message textarea scaled up: one field with `leadingContent` and `trailingContent` action slots docked inside it. Passing `leadingContent` takes the bottom-left slot, so the formatting keys stand down here.
Enter to continue
Composer — two leading actions
`leadingContent` takes more than one control: hand it a fragment and the bottom-left row spaces them. Here a `+` sits ahead of a model picker — the flat `<DropdownMenu items>` with `checked` on the current model.
Composer — rich text and triggers
`richText` is on by default: the marks render as you type, `⌘B` / `⌘I` / `⌘U` / `⌘⇧S` / `⌘E` / `⌘⇧8` / `⌘⇧7` work, and the seven keys dock bottom-left. The value underneath stays **markdown** — the panel below the field is the live string — so a `body` column stays text — note underline is `++like this++`, a markdown extension rather than CommonMark, so whatever renders these strings has to know the rule. `triggers` adds a menu on a character: `@` for people and `#` for a deliverable here, each with its own `onSearch`. ↑/↓ move, Enter picks, Escape closes. `onType` is the general tap — the row above the markdown is the caret's word and the marks under it, reported on every keystroke.
onTypeword: —marks: —
Kick-off notes — **bold**, *italic*, ++underline++, `code`, and a list:

- who is on it
- what ships first

Type @ or # below.
Composer — generator
`<Composer>` — the composer as a titled panel, every control docked inside the field: the `menu` bottom-left, `advanced` (with its count) and the primary submit bottom-right, `headingAside` beside the heading. The Advanced drawer body is whatever you pass. Enter submits; returning `false` from `onSubmit` shakes the field.

Create a design

2 of 5 generations used
Composer — generator, re-dressed
`mode` is the whole demo: one switch that re-dresses the panel — heading, submit label and surface follow the active side, and both strings cross-swap rather than cutting. The rest is props too — `menu` for the docked dropdown, `attachments` + `attachmentCount` for the paperclip and its count. Nothing is hand-wired: the import is `Composer` and `useState`.

Tags input
Multi-value input that commits each entry as a Squad Badge chip on Enter / Tab / comma. Backspace on an empty draft removes the previous chip; paste a comma- or newline-separated list to bulk-add.

Press Enter / Tab / , to add. Backspace to remove the last tag.

Sermon seriesEasterOutreach

The fourth tag is silently rejected.

VolunteersWorship
Duration input
Segmented time-duration field with configurable units (d/h/m/s). Segments auto-advance on fill and support keyboard navigation.

Output: 3600s

Output: 7384s

Output: 120s

Output: 90000s

Output: 3600–7200s

Output: 90–240s

Output: 120–240s

Repeatable input
A single input that grows into a list. Click the + ghost button next to any row to insert another input below it; the response is a `string[]`. Set `min` (defaults to 1) and an optional `max` to bound the row count.

Click “+ Add another” to add a URL. Outputs a string[].

Min 2, max 5.

Grouped repeatable input
`GroupedRepeatableInput` puts toggle chips across the top; every selected chip reveals its own `RepeatableInput` below, keyed by the option's `value`. Chips take a text `label`, an `icon`, or both (icon-only chips use `title` for the group heading and the accessible name). Rows are any input `type`, or a custom control via `renderRow` — that's exactly how the date picker's `mode="weekly"` builds its per-day time lists. `onChange` returns `Record<string, string[]>`; a key exists only while its chip is on. Pass `noneLabel` for an optional opt-out chip after the options — it and the real chips are mutually exclusive.

Pick a platform, then add every profile URL for it.

Icon-only chips — `title` names the group and labels the chip.

Loading
`loading` is one prop on every input — `Input` (plain, `search`, `collapsible`, `autocomplete`) and `TextField`. It shows a trailing spinner in the same slot as the clear (✕) or `iconTrailing`, and the two morph with the transitions.dev icon swap rather than cutting. While loading, hovering the spinner turns it back into the ✕ so a search can still be cleared mid-request.
Autocomplete
Pass `autocomplete` with a flat `items` array (`string[]` or `{ label, value }[]`) to turn the input into a suggest-as-you-type field. It filters live, reveals every option on focus, and shows a clear button once there's a value.
Autocomplete with descriptions
Add a `description` to any option to render a secondary line under the label — useful for disambiguating similar entries.
Autocomplete with icons
Add an `icon` to any option to render a leading glyph next to the label.
API Reference
Props exposed by each input primitive.

TextField

PropTypeDefaultDescription
labelstring—Renders an associated <Label> above the input.
captionstring—Helper copy shown below the input. Hidden when `error` is set.
errorstring—Error message — toggles the red focus ring + red helper line.
requiredboolean—Marks the label with a red asterisk.
iconLeadingComponentType<{ className?: string }>—Lucide icon component rendered inside the leading slot.
iconTrailingComponentType<{ className?: string }>—Lucide icon component rendered inside the trailing slot.
type"text" | "email" | "tel" | "password" | "url" | "number" | "search" | …"text"Native HTML type. `email` and `tel` get blur-time validation hooks.
invalidMessagestring—Error message to surface when the built-in email/tel validator rejects the value.
onValidityChange(valid: boolean) => void—Fires with the result of the email/tel validator on blur.

TextareaField

PropTypeDefaultDescription
labelstring—Renders an associated <Label> above the textarea.
captionstring—Helper copy shown below the textarea.
errorstring—Error message — toggles the red treatment.
rowsnumber3Initial visible row count.
leadingContentReactNode—Slot docked inside the field, bottom-left. Pass a fragment for more than one control — a `+` and a model picker, say — and they sit side by side, spaced.
richTextbooleanfalseSwap the textarea for the rich editor: bold, italic, underline, strikethrough, inline code and the two lists render as you type, with `⌘B` / `⌘I` / `⌘U` / `⌘⇧S` / `⌘E` / `⌘⇧8` / `⌘⇧7` and the keys docked bottom-left. `value` stays a **markdown string** — the same text a textarea would have carried — so a `body` column stays text and a feed can parse it back. Defaults to `true` on `<Composer>`. An explicit `leadingContent` replaces the whole bottom-left slot, these included.
showCharacterCountboolean—Render a live character count in the trailing slot.
characterCountnumber—Override the displayed character count.
trailingContentReactNode—Custom node rendered below-right (e.g. dictation button).
dictationbooleanfalseMount a browser-dictation mic button (Web Speech API) that streams transcribed phrases into the value. Hides itself where speech recognition is unsupported. On `<Composer>` it joins the docked action row; elsewhere it fills the trailing slot unless `trailingContent` overrides it.
voiceGlowbooleanfalseWith `dictation`, bloom a sound-reactive glow along the field's bottom edge while the mic is live, so a listening composer looks listening. It is drawn as a layer INSIDE the field's box (never a wrapper around it), so the focus ring is never clipped. With browser dictation it opens its own capture of the same mic (the Web Speech API hands out no stream); with a `transcriber` it reads that engine's stream. Needs a secure context — https or localhost.
transcriber{ supported, state, stream?, start(onText), stop, error? }—With `dictation`, a speech-to-text engine for the mic to drive instead of the browser's own — a streaming transcription API, say. The mic starts and stops it; each piece it hands to `onText(text, first)` is written into the field (a session's first piece spaced from the text already there, later pieces appended as they come, half-words included); `voiceGlow` reacts to its `stream`. `state` `starting`/`recording` reads as listening, `finishing` holds the button until it is `idle`. Left out, the field uses the Web Speech API.

Composer

PropTypeDefaultDescription
variant"panel" | "field""panel"`panel` is the titled generator panel — heading row, every control docked inside the field, the Advanced drawer underneath. `field` is the bare tall composer that only hosts `leadingContent` / `trailingContent`. Every TextareaField prop above works here too.
richTextbooleantrueThe rich editor, on here unlike on a bare TextareaField — a composer is where someone writes a message, a labelled textarea is where they fill in a form. Pass `false` for a plain textarea. `onChange` still receives an event whose `target.value` is the markdown, so existing handlers keep working; `ref` answers `focus()`, `value` and `setSelectionRange(end)` but is no longer a real textarea element.
formatsComposerFormat[]—Only these formats — `["bold", "italic", "underline"]` for a comment box that should stay plain. A `ComposerFormat` is any mark (`bold`, `italic`, `underline`, `strike`, `code`, `highlight`, `bulletList`, `orderedList`) or `heading`, `color`, `link`, `button`, `callout`. It limits the bar AND the editor: a format not listed has no key, no shortcut and no input rule, so `## ` stays text. Left out, every one. Read when the editor is created. Needs `richText`.
onType(state: ComposerTypingState) => void—Fires on every keystroke with where the writer is, not just what they wrote: `{ markdown, text, textBefore, word, active, empty, caret }` — the caret's word, the text before it, the marks under it, and the caret's viewport rect for anchoring anything of your own. `onChange` is the value; this is the tap a parent app hangs behaviour off. Needs `richText`.
triggersComposerTrigger[]—Characters that open a menu mid-sentence — `@` for people, `#` for a tag. The composer finds the trigger, tracks the query typed after it, places the menu at the caret and drives ↑/↓/Enter/Escape; the rows and what gets written are yours. Each entry is `{ char, onSearch, onSelect?, render?, atWordStart?, maxQuery? }`. Needs `richText`.
headingReactNode—panel heading. A string is wrapped in an <h2> and labels the section.
headingAsideReactNode—right side of the heading row; the allowance pill, typically.
menuGeneratorMenu—the menu docked bottom-left: `{ label, value, onChange, options, autoLabel, autoDescription, hint, align }`. `value: null` selects the auto entry; pass `autoLabel: null` to drop it.
advancedReactNode—the Advanced drawer body. Entirely yours: any fields, any layout. Omit it and the button disappears.
advancedLabelstring"Advanced"label on the drawer toggle (sr-only on a phone).
advancedCountnumber0count badge on the toggle, so a shut drawer can't hide a control that is in force.
advancedOpenboolean—controlled drawer state. Omit for uncontrolled.
onAdvancedOpenChange(open: boolean) => void—fires whenever the drawer opens or closes.
submitLabelstring"Submit"label on the primary button (sr-only on a phone).
submitIconReactNodean arrowicon to the right of the submit label. Pass `null` for a label-only button.
onSubmit() => void | boolean—fires on the button and on Enter. Return `false` to refuse: the field shakes instead. Omit it and the button disappears.
submittingbooleanfalseswaps the submit label for a spinner and locks the field.
submitDisabledbooleanfalsedisable submit without disabling the field.
submitOnEnterbooleantrueEnter submits, Shift+Enter is a newline.
autoGrowbooleantrueGrow the field to its content instead of scrolling it, up to the field's max-height (`max-h-64` by default) — past that it scrolls. Pass your own `max-h-*` in `className` to change the ceiling.
attachmentsbooleanfalsemount a paperclip in the docked action row that opens the native file picker. Holding and rendering the files stays yours.
onAttach(files: File[]) => void—fires with the files chosen from the picker. The input resets after each pick, so the same file twice in a row still fires.
attachmentAcceptstring—`accept` on the picker, e.g. "image/*,.pdf".
attachmentMultiplebooleantruelet the picker take more than one file.
attachmentFilesreadonly File[]—The files to show as outline pills across the top of the field — the SDK's outline Button, with the name, size and a × each; they wrap onto more rows as needed. Hand back what `onAttach` gave you. With pills in the box the field's border moves to the wrapper, so they and the writing surface read as one field.
onRemoveAttachment(index: number) => void—Fires with the index of the card whose × was pressed. Omit it and the cards have no remove button.
attachmentIcon(file: File, index: number) => ReactNode—The glyph a card leads with, in place of the file icon — an integration's logo for an item picked from it. Return undefined to keep the file icon.
attachmentPreview(file: File, index: number) => ReactNode—What a card shows in a popover when its name is hovered or clicked — the item's details. Return null for a card with nothing more to say.
attachmentCountnumberattachmentFiles.lengthCount shown beside the paperclip, for when you're tracking a number without handing over the files. It hides itself once `attachmentFiles` is putting cards across the top of the field — there it would only be saying the same thing twice.
attachmentLabelstring"Attach files"accessible name and tooltip on the paperclip.
panelBackground"muted" | "plain" | "none" | "striped""muted"the panel surface. `muted` is the tinted card, `plain` drops the tint, `none` drops the surface entirely so the field stands alone, `striped` is the dashed-edge, diagonally hatched treatment.
panelColorstring—the colour the panel is built from. Any CSS colour: it tints the fill, and on `striped` it draws the dashes and the hatching too. Omit it for the theme's muted surface.
modeComposerMode—A two-state switch docked in the heading row that re-dresses the panel: `{ value?, defaultValue?, onChange?, label?, on, off }`, where each side is a `{ heading, submitLabel, panelBackground, panelColor }` face and anything omitted falls back to the top-level prop. Controlled with `value`, uncontrolled off `defaultValue`. It supplies its own `headingAside`, so pass one only to override it. The heading and the submit label cross-swap between faces rather than cutting.
panelClassNamestring—extra classes on the panel element, for whatever the props above don't reach.

NumberInput

PropTypeDefaultDescription
valuenumber—Controlled value.
defaultValuenumber0Uncontrolled initial value. When value is 0 the input displays empty (placeholder shown).
onValueChange(value: number) => void—Fires with the parsed numeric value on every change.
minnumber—Lower bound — decrement button disables when reached.
maxnumber—Upper bound — increment button disables when reached.
stepnumber1Amount added/subtracted per stepper click.
placeholderstring"0"Placeholder shown when the value is 0 or empty.
captionReactNode—Helper copy shown below the input via FieldHint. Hidden when `error` is set.
errorboolean | string—Error state. `true` applies the destructive ring only; a string also renders the inline message (`text-xs mt-1.5 text-red-500`) below the input.
disabledboolean—Dims the field and prevents interaction.

FileSizeInput

PropTypeDefaultDescription
value{ width, height, unit }—Controlled value. Width and height are number | "" — unit is one of in/cm/mm/px/pt.
onChange(value) => void—Fires whenever any of the three sub-fields change.
allowedUnitsArray<"in" | "cm" | "mm" | "px" | "pt">—Restrict the unit dropdown — defaults to all five.
idstring—Forwarded to the width input for label association.

TagsInput

PropTypeDefaultDescription
valuestring[]—Controlled tag list.
onChange(value: string[]) => void—Fires whenever a tag is added or removed.
maxnumber—Cap the chip count. Extras are silently rejected.
placeholderstring—Placeholder rendered inside the draft input.
captionstring—Helper text below the field.

DurationInput

PropTypeDefaultDescription
unitsArray<"d" | "h" | "m" | "s">["h","m","s"]Which segments to render, left-to-right.
valuenumber—Controlled value in total seconds.
defaultValuenumber0Uncontrolled initial value in total seconds.
onValueChange(totalSeconds: number) => void—Fires with the parsed total seconds on every change.
disabledboolean—Dims the field and prevents interaction.

RepeatableInput

PropTypeDefaultDescription
valuestring[]—Controlled list of row values. The component outputs `string[]`.
defaultValuestring[]—Uncontrolled initial list. Padded with empty strings up to `min`.
onChange(value: string[]) => void—Fires whenever a row is added, removed, or edited.
typeHTMLInputType"text"Input type for every row — forwarded to the underlying TextField.
placeholderstring—Placeholder applied to each row's input.
minnumber1Minimum number of rows. The remove button hides when at the floor.
maxnumber—Optional maximum. The add-row link hides once reached.
addLabelstring"+ Add another"Text of the add-row link rendered under the list.
renderRow(row) => ReactNode—Render a custom control per row instead of the default TextField — receives `{ value, onChange, index, id, disabled, error }`. Add/remove behaviour is unchanged; the row hugs the custom control instead of stretching.
labelstring—Renders the field label above the stack of inputs.
captionstring—Helper text rendered directly under the label.
errorstring—Error message — toggles the red treatment on the first row.
requiredboolean—Marks the label with a red asterisk.
disabledboolean—Dims every row and disables the +/− buttons.

GroupedRepeatableInput

PropTypeDefaultDescription
optionsGroupedRepeatableOption[]—The chips, in render order — `{ value, label?, icon?, title?, disabled? }`. `value` keys the group's rows; `label` is the chip text (omit for an icon-only chip); `icon` renders before the label; `title` heads the group and names an icon-only chip.
valueRecord<string, string[]>—Controlled value. A key exists only while its chip is on; toggling a chip off drops its rows.
defaultValueRecord<string, string[]>—Uncontrolled initial value.
onChange(value: Record<string, string[]>) => void—Fires on every chip toggle, row edit, add, and remove.
newRowValuestring""Row value seeded when a chip is switched on.
columns1 | 2 | 3 | 43Group columns at the widest breakpoint (always 1 on mobile).
noneLabelReactNode—Renders an optional opt-out chip after the options (“We're not online yet”). It and the real chips are mutually exclusive — picking it clears every group, picking a platform clears it. Omit to hide it.
noneIconComponentType<{ className?: string }>—Icon rendered in the opt-out chip.
none / defaultNone / onNoneChangeboolean | (none: boolean) => void—Controlled, uncontrolled, and change handler for the opt-out chip.
type / placeholder / iconLeading / min / max / addLabel / renderRowRepeatableInputProps—Forwarded to every group's RepeatableInput — so the rows can be any input type, or a custom control via `renderRow`.
label / caption / error / required / disabledstring | boolean—Field chrome above the chips; `error` also reddens the groups.