Composition-friendly text fields with labels, descriptions, and inline validation.
Import
import { TextField } from "heroui-solid";Usage
import { Input, Label, TextField } from "heroui-solid"
export function Basic() {
return (
<TextField class="w-full max-w-64" name="email">
<Label>Email</Label>
<Input placeholder="Enter your email" type="email" />
</TextField>
)
}Anatomy
import { TextField, Label, Input, Description, FieldError } from "heroui-solid";
export default () => (
<TextField>
<Label />
<Input />
<Description />
<FieldError />
</TextField>
);TextField combines label, input, description, and error into a single accessible component. For standalone inputs, use Input or TextArea.
With Description
import { Description, Input, Label, TextField } from "heroui-solid"
export function WithDescription() {
return (
<TextField class="w-full max-w-64" name="username">
<Label>Username</Label>
<Input placeholder="Enter username" />
<Description>Choose a unique username for your account</Description>
</TextField>
)
}Required Field
import { Description, Input, Label, TextField } from "heroui-solid"
export function Required() {
return (
<TextField isRequired class="w-full max-w-64" name="fullName">
<Label>Full Name</Label>
<Input placeholder="John Doe" />
<Description>This field is required</Description>
</TextField>
)
}Validation
Use isInvalid together with FieldError to surface validation messages.
import {
Description,
FieldError,
Input,
Label,
TextArea,
TextField
} from "heroui-solid"
import { createSignal, Show } from "solid-js"
export function Validation() {
const [username, setUsername] = createSignal("")
const [bio, setBio] = createSignal("")
const isUsernameInvalid = () => username().length > 0 && username().length < 3
const isBioInvalid = () => bio().length > 0 && bio().length < 20
return (
<div class="flex w-full max-w-64 flex-col gap-4">
<TextField
isRequired
isInvalid={isUsernameInvalid()}
name="username"
value={username()}
onChange={setUsername}
>
<Label>Username</Label>
<Input placeholder="jane_doe" />
<Show
when={isUsernameInvalid()}
fallback={
<Description>
Choose a unique username for your profile.
</Description>
}
>
<FieldError>Username must be at least 3 characters.</FieldError>
</Show>
</TextField>
<TextField
isRequired
isInvalid={isBioInvalid()}
name="bio"
value={bio()}
onChange={setBio}
>
<Label>Bio</Label>
<TextArea placeholder="Tell us about yourself..." />
<Show
when={isBioInvalid()}
fallback={
<Description>
Minimum 20 characters ({bio().length}/20).
</Description>
}
>
<FieldError>Bio must contain at least 20 characters.</FieldError>
</Show>
</TextField>
</div>
)
}Controlled
Control the value to synchronize counters, previews, or formatting.
import { Description, Input, Label, TextArea, TextField } from "heroui-solid"
import { createSignal } from "solid-js"
export function Controlled() {
const [name, setName] = createSignal("")
const [bio, setBio] = createSignal("")
return (
<div class="flex w-full max-w-64 flex-col gap-4">
<TextField name="name" value={name()} onChange={setName}>
<Label>Display name</Label>
<Input placeholder="Jane" />
<Description>Characters: {name().length}</Description>
</TextField>
<TextField name="bio" value={bio()} onChange={setBio}>
<Label>Bio</Label>
<TextArea placeholder="Tell us about yourself..." />
<Description>Characters: {bio().length} / 200</Description>
</TextField>
</div>
)
}Error Message
import { FieldError, Input, Label, TextField } from "heroui-solid"
export function WithError() {
return (
<TextField isInvalid class="w-full max-w-64" name="email">
<Label>Email</Label>
<Input placeholder="user@example.com" type="email" />
<FieldError>Please enter a valid email address</FieldError>
</TextField>
)
}Disabled State
import { Description, Input, Label, TextField } from "heroui-solid"
export function Disabled() {
return (
<TextField
isDisabled
class="w-full max-w-64"
name="accountId"
value="USR-12345"
>
<Label>Account ID</Label>
<Input placeholder="Auto-generated" />
<Description>This field cannot be edited</Description>
</TextField>
)
}TextArea
Use TextArea instead of Input for multiline content.
import { Description, Label, TextArea, TextField } from "heroui-solid"
export function TextAreaExample() {
return (
<TextField class="w-full max-w-64" name="message">
<Label>Message</Label>
<TextArea placeholder="Write your message here..." rows={4} />
<Description>Maximum 500 characters</Description>
</TextField>
)
}Input Types
HeroUI React sets type on the TextField root; in this port type goes on the
Input element instead (Kobalte keeps input attributes on the input).
import { Input, Label, TextField } from "heroui-solid"
export function InputTypes() {
return (
<div class="flex w-full max-w-64 flex-col gap-4">
<TextField name="password">
<Label>Password</Label>
<Input placeholder="••••••••" type="password" />
</TextField>
<TextField name="age">
<Label>Age</Label>
<Input max="150" min="0" placeholder="21" type="number" />
</TextField>
<TextField name="email">
<Label>Email</Label>
<Input placeholder="user@example.com" type="email" />
</TextField>
<TextField name="website">
<Label>Website</Label>
<Input placeholder="https://example.com" type="url" />
</TextField>
<TextField name="phone">
<Label>Phone</Label>
<Input placeholder="+1 (555) 000-0000" type="tel" />
</TextField>
</div>
)
}Full Width
import { FieldError, Input, Label, TextField } from "heroui-solid"
export function FullWidth() {
return (
<div class="w-[400px] space-y-4">
<TextField fullWidth name="name">
<Label>Your name</Label>
<Input placeholder="John" />
</TextField>
<TextField fullWidth isInvalid isRequired name="password">
<Label>Password</Label>
<Input type="password" />
<FieldError>Password must be longer than 8 characters</FieldError>
</TextField>
</div>
)
}In Surface
When used inside a Surface component, use variant="secondary" on the TextField to apply the lower emphasis variant suitable for surface backgrounds.
import {
Description,
Input,
Label,
Surface,
TextArea,
TextField
} from "heroui-solid"
export function OnSurface() {
return (
<Surface class="flex w-full min-w-[340px] flex-col gap-4 rounded-3xl p-6">
<TextField name="name" variant="secondary">
<Label>Your name</Label>
<Input fullWidth placeholder="John" />
<Description>We'll never share this with anyone else</Description>
</TextField>
<TextField name="email" variant="secondary">
<Label>Email</Label>
<Input fullWidth placeholder="john@example.com" type="email" />
</TextField>
<TextField name="bio" variant="secondary">
<Label>Bio</Label>
<TextArea fullWidth placeholder="Tell us about yourself..." rows={4} />
<Description>Minimum 4 rows</Description>
</TextField>
</Surface>
)
}Related Components
- Input: Single-line text input
- TextArea: Multiline text input
- Label: Accessible label for form controls
Styling
Passing Tailwind CSS classes
import { TextField, Label, Input, Description } from "heroui-solid";
function CustomTextField() {
return (
<TextField class="gap-2 rounded-xl border border-border/60 bg-surface-secondary p-4 shadow-sm">
<Label class="text-sm font-semibold text-foreground">Project name</Label>
<Input class="rounded-lg border border-border/60 bg-surface-secondary px-3 py-2" />
<Description class="text-xs text-muted">
Keep it short and memorable.
</Description>
</TextField>
);
}Customizing the component classes
TextField has minimal default styling. Override the .textfield class to customize the container styling.
@layer components {
.textfield {
@apply flex flex-col gap-1;
}
/* When invalid, the description is hidden automatically */
.textfield[data-invalid="true"] [data-slot="description"],
.textfield[aria-invalid="true"] [data-slot="description"] {
@apply hidden;
}
/* Description has default padding */
.textfield [data-slot="description"] {
@apply px-1;
}
}CSS Classes
.textfield– Root container with minimal styling (flex flex-col gap-1).textfield--full-width– Applied whenfullWidthis set; also stretches child inputs
Note: Child components (Label, Input, TextArea, Description, FieldError) have their own CSS classes and styling. See their respective documentation for customization options.
Interactive States
TextField automatically manages these data attributes based on its state:
- Invalid:
[data-invalid="true"]- Automatically hides the description slot when invalid - Required:
[data-required="true"]- Shows the required asterisk on a child Label - Disabled:
[data-disabled="true"]- Applied whenisDisabledis true - Read Only:
[data-readonly="true"]- Applied whenisReadOnlyis true
API Reference
TextField Props
TextField is built on Kobalte's TextField primitive.
Base Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "primary" | "secondary" | "primary" | Variant applied to child Input/TextArea via context |
fullWidth | boolean | false | Whether the text field should take full width of its container |
id | string | - | The element's unique identifier |
class | string | - | Additional CSS classes |
children | JSX.Element | - | Child components (Label, Input, etc.) |
as | ValidComponent | "div" | Overrides the default DOM element (Kobalte polymorphic) |
Validation Props
| Prop | Type | Default | Description |
|---|---|---|---|
isRequired | boolean | false | Whether user input is required before form submission |
isInvalid | boolean | - | Whether the value is invalid |
Value Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Current value (controlled) |
defaultValue | string | - | Default value (uncontrolled) |
onChange | (value: string) => void | - | Handler called when the value changes |
State Props
| Prop | Type | Default | Description |
|---|---|---|---|
isDisabled | boolean | - | Whether the input is disabled |
isReadOnly | boolean | - | Whether the input can be selected but not changed |
Form Props
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | - | Name of the input element, for HTML form submission |
Accessibility Props
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | - | Accessibility label when no visible label is present |
aria-labelledby | string | - | ID of elements that label this field |
aria-describedby | string | - | ID of elements that describe this field |
Composition Components
TextField works with these separate components that should be imported and used directly:
- Label - Field label component from
heroui-solid - Input - Single-line text input from
heroui-solid - TextArea - Multi-line text input from
heroui-solid - Description - Helper text component from
heroui-solid - FieldError - Validation error message from
heroui-solid
Differences from HeroUI React
type(and other input attributes likemin/max/autocomplete) go onInput, not on the TextField root.- No render-prop
children/classNameand norenderprop — use plain children and Kobalte's polymorphicasfor custom elements. - No
validate/validationBehavior/validationErrorsprops — driveisInvalidyourself (see the Validation example) or use a form library. autoFocusbelongs on theInputelement.
Last updated: 7/19/26, 3:27 AM