Browse docs
Browse docs
A single-line input with everything you'd otherwise hand-roll: typed label, helper text, error state, focus ring, focus management, and a clean integration with <DashForm> (React Hook Form under the hood).
import { TextField } from '@dashforge/tw';
<TextField name="email" label="Email" type="email" required />import { DashForm } from '@dashforge/forms';
import { TextField, Button } from '@dashforge/tw';
export function SignIn() {
return (
<DashForm onSubmit={signIn}>
<TextField name="email" type="email" label="Email" required />
<TextField name="password" type="password" label="Password" required
rules={{ minLength: { value: 8, message: 'At least 8 characters' } }}
/>
<Button type="submit" color="primary">Sign in</Button>
</DashForm>
);
}Standalone (no form):
const [value, setValue] = useState('');
<TextField label="Search" value={value} onChange={(e) => setValue(e.target.value)} />3–24 characters, letters and numbers.
import { TextField, Stack } from '@dashforge/tw';
<Stack gap={3}>
<TextField
name="email"
type="email"
label="Email"
placeholder="[email protected]"
/>
<TextField
name="username"
label="Username"
required
placeholder="jdoe"
helperText="3–24 characters, letters and numbers."
/>
</Stack><TextField
name="email"
type="email"
label="Email"
required
rules={{
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Enter a valid email address',
},
}}
/>rules is the React Hook Form rules object. The error message is rendered automatically in the helperText slot when the field is touched AND invalid.
Minimum 12 characters.
Enter a valid email address.
import { TextField, Stack } from '@dashforge/tw';
<Stack gap={3}>
<TextField
name="password"
type="password"
label="Password"
helperText="Minimum 12 characters."
/>
<TextField
name="email"
type="email"
label="Email"
defaultValue="not-an-email"
error
helperText="Enter a valid email address."
/>
</Stack><TextField
label="Password"
type="password"
helperText="At least 8 characters, 1 number, 1 symbol."
/>
<TextField
label="Email"
error
helperText="That email is already registered."
/>Helper text turns red on error={true}; aria-invalid and aria-describedby are wired automatically.
import { TextField, Stack } from '@dashforge/tw';
<Stack gap={3}>
<TextField name="small" size="sm" label="Small" placeholder="size=sm" />
<TextField name="medium" size="md" label="Medium (default)" placeholder="size=md" />
<TextField name="large" size="lg" label="Large" placeholder="size=lg" />
<TextField name="locked" label="Disabled" defaultValue="cannot edit" disabled />
</Stack><TextField size="sm" label="Compact" />
<TextField size="md" label="Default" />
<TextField size="lg" label="Spacious" /><TextField
label="Price"
type="number"
slotProps={{
prefix: { children: '#x27; },
suffix: { children: 'USD' },
}}
/>Inline adornments live in dedicated slots so they don't compete with the input's own padding.
<TextField label="Locked" value="cannot edit" disabled />
<TextField label="Reference" value="DF-2026-04812" readOnly />disabled removes the field from tab order; readOnly keeps it tabbable for copy/paste.
<TextField
name="adminNote"
label="Internal note"
access={{ requires: 'workspace.admin', when: 'denied:readonly' }}
/>Three when modes ('denied:hide' | 'denied:disable' | 'denied:readonly') match the Button pattern. The bridge layer applies the right one without re-rendering the form.
Combine with the form engine to show/hide based on another field's value:
<RadioGroup name="contactMethod" options={[{ value: 'email' }, { value: 'sms' }]} />
<TextField
name="email"
type="email"
visibility={{ when: { field: 'contactMethod', equals: 'email' } }}
/>
<TextField
name="phone"
type="tel"
visibility={{ when: { field: 'contactMethod', equals: 'sms' } }}
/>See Form System → Reactions for the full reactive engine.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Field name. Registers with <DashForm> if present. |
label | ReactNode | — | Field label. Renders above the input by default. |
helperText | ReactNode | — | Helper text below the input. Auto-replaced by error message when invalid. |
type | 'text' | 'email' | 'password' | 'tel' | 'url' | 'number' | ... | 'text' | HTML input type. |
size | 'sm' | 'md' | 'lg' | 'md' | Density. |
required | boolean | false | Marks the label + adds required RHF rule. |
disabled | boolean | false | Disables the input and removes it from tab order. |
readOnly | boolean | false | Read-only but still tabbable. |
error | boolean | false | Force error state (red border + helper text). |
rules | RegisterOptions (RHF) | — | Validation rules. Used by <DashForm> integration. |
visibility | VisibilitySpec | — | Reactive visibility (form-engine driven). See Form System. |
access | AccessSpec | — | RBAC gating. |
slotProps | { root?, label?, input?, helperText?, prefix?, suffix? } | — | Per-slot overrides. |
className | string | — | Merged onto root via tailwind-merge. |
| ...rest | React.InputHTMLAttributes | — | All native input props pass through. |
root · label · input · helperText · prefix · suffix · wrapper
Each slot is overridable via slotProps AND inspectable via the tailwind-variants export:
import { textFieldVariants } from '@dashforge/tw';
const cn = textFieldVariants({ size: 'md', error: true });
// cn → { root, label, input, helperText, ... } class stringstext-neutral-700 dark:text-neutral-300 directly — no FOUC during SSR hydration.bg / text color (no yellow flash in Chrome).label slot as a <span className="text-danger-600">*</span> — override the asterisk by passing your own label slot.NumberField (Tier-2, coming soon). For now, <TextField type="number"> gives you the native input only.