Skip to content
Lenso UI

TextField

Composition-friendly text fields with labels, descriptions, and inline validation

Usage

import { TextField } from '@lenso/ui';
"use client";
// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 256 } });
export function Basic() {  return (    <TextField name="email" xstyle={styles.field}>      <Label>Email</Label>      <Input type="email" placeholder="Enter your email" />    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anatomy

import {TextField, Label, Input, Description, FieldError} from '@lenso/ui';
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.

Examples

In Surface

When used inside a Surface component, use variant="secondary" on Input or TextArea components to apply the lower emphasis variant suitable for surface backgrounds.

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, Input, Label, Surface, TextArea, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: {    display: "flex",    width: "100%",    minWidth: 340,    flexDirection: "column",    gap: 16,    borderRadius: 24,    padding: 24,  },});export function OnSurface() {  return (    <Surface xstyle={styles.root}>      <TextField name="name">        <Label>Your name</Label>        <Input fullWidth variant="secondary" placeholder="John" />        <Description>We'll never share this with anyone else</Description>      </TextField>      <TextField name="email">        <Label>Email</Label>        <Input type="email" fullWidth variant="secondary" placeholder="[email protected]" />      </TextField>      <TextField name="bio">        <Label>Bio</Label>        <TextArea fullWidth variant="secondary" placeholder="Tell us about yourself..." rows={4} />        <Description>Minimum 4 rows</Description>      </TextField>    </Surface>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Description

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 256 } });export function WithDescription() {  return (    <TextField xstyle={styles.field} name="username">      <Label>Username</Label>      <Input placeholder="Enter username" />      <Description>Choose a unique username for your account</Description>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Required Field

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 256 } });export function Required() {  return (    <TextField xstyle={styles.field} name="fullName">      <Label>Full Name</Label>      <Input required placeholder="John Doe" />      <Description>This field is required</Description>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Disabled State

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 256 } });export function Disabled() {  return (    <TextField disabled xstyle={styles.field} name="accountId">      <Label>Account ID</Label>      <Input value="USR-12345" placeholder="Auto-generated" />      <Description>This field cannot be edited</Description>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Full Width

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { FieldError, Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { display: "flex", width: 400, flexDirection: "column", gap: 16 },});export function FullWidth() {  return (    <div {...stylex.props(styles.root)}>      <TextField fullWidth name="name">        <Label>Your name</Label>        <Input placeholder="John" />      </TextField>      <TextField fullWidth invalid name="password">        <Label>Password</Label>        <Input required type="password" />        <FieldError match>Password must be longer than 8 characters</FieldError>      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Validation

Use isInvalid together with FieldError to surface validation messages.

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, FieldError, Input, Label, TextArea, TextField } from "@lenso/ui";import * as React from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { display: "flex", width: "100%", maxWidth: 256, flexDirection: "column", gap: 16 },});export function Validation() {  const [username, setUsername] = React.useState("");  const [bio, setBio] = React.useState("");  const isUsernameInvalid = username.length > 0 && username.length < 3;  const isBioInvalid = bio.length > 0 && bio.length < 20;  return (    <div {...stylex.props(styles.root)}>      <TextField invalid={isUsernameInvalid} name="username">        <Label>Username</Label>        <Input required value={username} onValueChange={setUsername} placeholder="jane_doe" />        {isUsernameInvalid ? (          <FieldError match>Username must be at least 3 characters.</FieldError>        ) : (          <Description style={{ display: "block" }}>            Choose a unique username for your profile.          </Description>        )}      </TextField>      <TextField invalid={isBioInvalid} name="bio">        <Label>Bio</Label>        <TextArea          required          value={bio}          onValueChange={setBio}          placeholder="Tell us about yourself..."        />        {isBioInvalid ? (          <FieldError match>Bio must contain at least 20 characters.</FieldError>        ) : (          <Description style={{ display: "block" }}>            Minimum 20 characters ({bio.length}/20).          </Description>        )}      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Controlled

Control the value to synchronize counters, previews, or formatting.

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, Input, Label, TextArea, TextField } from "@lenso/ui";import * as React from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { display: "flex", width: "100%", maxWidth: 256, flexDirection: "column", gap: 16 },});export function Controlled() {  const [name, setName] = React.useState("");  const [bio, setBio] = React.useState("");  return (    <div {...stylex.props(styles.root)}>      <TextField name="name">        <Label>Display name</Label>        <Input placeholder="Jane" value={name} onValueChange={setName} />        <Description>Characters: {name.length}</Description>      </TextField>      <TextField name="bio">        <Label>Bio</Label>        <TextArea placeholder="Tell us about yourself..." value={bio} onValueChange={setBio} />        <Description>Characters: {bio.length} / 200</Description>      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Error Message

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { FieldError, Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 256 } });export function WithError() {  return (    <TextField invalid xstyle={styles.field} name="email">      <Label>Email</Label>      <Input type="email" placeholder="[email protected]" />      <FieldError match>Please enter a valid email address</FieldError>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

TextArea

Use TextArea instead of Input for multiline content.

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Description, Label, TextArea, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 256 } });export function TextAreaExample() {  return (    <TextField xstyle={styles.field} name="message">      <Label>Message</Label>      <TextArea placeholder="Write your message here..." rows={4} />      <Description>Maximum 500 characters</Description>    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Input Types

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { display: "flex", width: "100%", maxWidth: 256, flexDirection: "column", gap: 16 },});export function InputTypes() {  return (    <div {...stylex.props(styles.root)}>      <TextField name="password">        <Label>Password</Label>        <Input type="password" placeholder="••••••••" />      </TextField>      <TextField name="age">        <Label>Age</Label>        <Input type="number" max="150" min="0" placeholder="21" />      </TextField>      <TextField name="email">        <Label>Email</Label>        <Input type="email" placeholder="[email protected]" />      </TextField>      <TextField name="website">        <Label>Website</Label>        <Input type="url" placeholder="https://example.com" />      </TextField>      <TextField name="phone">        <Label>Phone</Label>        <Input type="tel" placeholder="+1 (555) 000-0000" />      </TextField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Render Function

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ field: { width: "100%", maxWidth: 256 } });export function RenderFunction() {  return (    <TextField      xstyle={styles.field}      name="email"      render={(props) => <div {...props} data-custom="foo" />}    >      <Label>Email</Label>      <Input type="email" placeholder="Enter your email" />    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Customization

Tailwind CSS

"use client";// Adapted from HeroUI v3.2.6 (e385ac202b2cdb94b1bf6fa76d32c31c8259cc5e), Apache-2.0.import { Input, Label, TextField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({  root: { width: "100%", maxWidth: 256, gap: 6 },  label: {    fontWeight: 500,    color: { default: "oklch(26.9% 0 0)", ':is(.dark *, [data-theme="dark"] *)': "oklch(97% 0 0)" },  },  input: {    fontSize: 14,    borderRadius: 12,    borderWidth: 1,    borderStyle: "solid",    borderColor: "color-mix(in oklab, var(--border) 80%, transparent)",    backgroundColor: "var(--surface)",    color: { default: "oklch(26.9% 0 0)", ':is(.dark *, [data-theme="dark"] *)': "oklch(97% 0 0)" },    boxShadow: {      default: "0 1px 2px 0 rgb(0 0 0 / .05), 0 0 0 1px rgb(0 0 0 / .05)",      ":focus-visible": "0 1px 2px 0 rgb(0 0 0 / .05), 0 0 0 2px oklch(70.8% 0 0 / .25)",      ':is(.dark *, [data-theme="dark"] *)':        "0 1px 2px 0 rgb(0 0 0 / .05), 0 0 0 1px rgb(255 255 255 / .1)",      ':is(.dark *, [data-theme="dark"] *):focus-visible':        "0 1px 2px 0 rgb(0 0 0 / .05), 0 0 0 2px oklch(55.6% 0 0 / .3)",    },    transitionProperty: "box-shadow, border-color",    transitionDuration: "150ms",    "::placeholder": {      color: {        default: "oklch(70.8% 0 0)",        ':is(.dark *, [data-theme="dark"] *)': "oklch(55.6% 0 0)",      },    },  },});export function CustomStyles() {  return (    <TextField xstyle={styles.root} name="email">      <Label xstyle={styles.label}>Email</Label>      <Input type="email" xstyle={styles.input} placeholder="[email protected]" />    </TextField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

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;  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

  • .textfield – Root container with minimal styling (flex flex-col gap-1)

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"] or [aria-invalid="true"] - Automatically hides the description slot when invalid
  • Disabled: [data-disabled="true"] - Applied when isDisabled is true
  • Focus Within: [data-focus-within="true"] - Applied when any child input is focused
  • Focus Visible: [data-focus-visible="true"] - Applied when focus is visible (keyboard navigation)

Additional attributes are available through render props (see TextFieldRenderProps below).

API Reference

TextField

TextField inherits all props from React Aria's TextField component.

Base Props

PropTypeDefaultDescription
childrenReact.ReactNode | (values: TextFieldRenderProps) => React.ReactNode-Child components (Label, Input, etc.) or render function.
classNamestring | (values: TextFieldRenderProps) => string-CSS classes for styling, supports render props.
styleReact.CSSProperties | (values: TextFieldRenderProps) => React.CSSProperties-Inline styles, supports render props.
fullWidthbooleanfalseWhether the text field should take full width of its container
idstring-The element's unique identifier.
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, TextFieldRenderProps>-Overrides the default DOM element with a custom render function.

Validation Props

PropTypeDefaultDescription
isRequiredbooleanfalseWhether user input is required before form submission.
isInvalidboolean-Whether the value is invalid.
validate(value: string) => ValidationError | true | null | undefined-Custom validation function.
validationBehavior'native' | 'aria''native'Whether to use native HTML form validation or ARIA attributes.
validationErrorsstring[]-Server-side validation errors.

Value Props

PropTypeDefaultDescription
valuestring-Current value (controlled).
defaultValuestring-Default value (uncontrolled).
onChange(value: string) => void-Handler called when the value changes.

State Props

PropTypeDefaultDescription
isDisabledboolean-Whether the input is disabled.
isReadOnlyboolean-Whether the input can be selected but not changed.

Form Props

PropTypeDefaultDescription
namestring-Name of the input element, for HTML form submission.
autoFocusboolean-Whether the element should receive focus on render.

Accessibility Props

PropTypeDefaultDescription
aria-labelstring-Accessibility label when no visible label is present.
aria-labelledbystring-ID of elements that label this field.
aria-describedbystring-ID of elements that describe this field.
aria-detailsstring-ID of elements with additional details.

Composition Components

TextField works with these separate components that should be imported and used directly:

  • Label - Field label component from @lenso/ui
  • Input - Single-line text input from @lenso/ui
  • TextArea - Multi-line text input from @lenso/ui
  • Description - Helper text component from @lenso/ui
  • FieldError - Validation error message from @lenso/ui

Each of these components has its own props API. Use them directly within TextField for composition:

<TextField isRequired isInvalid={hasError}>  <Label>Email Address</Label>  <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />  <Description>We'll never share your email.</Description>  <FieldError>Please enter a valid email address.</FieldError></TextField>

Render Props

When using render props with className, style, or children, these values are available:

PropTypeDescription
isDisabledbooleanWhether the field is disabled.
isInvalidbooleanWhether the field is currently invalid.
isReadOnlybooleanWhether the field is read-only.
isRequiredbooleanWhether the field is required.
isFocusedbooleanWhether the field is currently focused (DEPRECATED - use isFocusWithin).
isFocusWithinbooleanWhether any child element is focused.
isFocusVisiblebooleanWhether focus is visible (keyboard navigation).

See upstream TextField showcases. Product showcases are not part of the local component runtime.