Text fields

Text fields let users enter text into a UI

Usage

Use a text field when someone needs to enter text into a UI.

Instalation

Run the following command:

npm i @radix-ui/react-label

Copy and paste the following code into your project.

import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'

import { cn } from '@/lib/utils'

const commonStyle =
  'pointer-events-none absolute z-10 flex select-none text-label-sm font-normal leading-tight text-onSurfaceVariant duration-200 peer-placeholder-shown/input:text-body-lg peer-placeholder-shown/input:text-onSurfaceVariant/70 peer-focus/input:text-label-sm peer-focus/input:leading-tight peer-focus/input:text-primary peer-disabled/input:!text-onSurfaceVariant/38 group-data-[invalid]/container:text-error'

const outlinedLabel = [
  'h-full w-full left-0 top-[-6px] mr-1 peer-placeholder-shown/input:leading-[4.2]',
  'peer-focus/input:before:mr-1 peer-has-[*]/icon:peer-placeholder-shown/input:before:mr-9 peer-has-[*]/icon:peer-focus/input:before:mr-1',

  /** Before **/
  'before:pointer-events-none before:mr-1 before:mt-[6px] before:box-border before:block before:h-full before:w-3.5 before:rounded-l-sm before:border-outline before:duration-200 before:transition-all before:border-t before:border-l',

  'peer-placeholder-shown/input:before:border-transparent',

  'peer-focus/input:before:border-primary peer-focus/input:before:border-l-2 peer-focus/input:before:border-t-2',

  'group-data-[invalid]/container:before:border-error group-data-[invalid]/container:before:border-l-2 group-data-[invalid]/container:before:border-t-2',

  'peer-disabled/input:before:border-t-onSurface/12 peer-disabled/input:before:border-l-onSurface/12 peer-disabled/input:peer-placeholder-shown/input:before:border-transparent',

  /** After **/
  'after:h-full after:pointer-events-none after:ml-1 after:mt-[6px] after:box-border after:block after:flex-grow after:rounded-r-sm after:border-r after:border-t after:border-outline duration-200 after:transition-all',

  'peer-placeholder-shown/input:after:border-transparent',

  'peer-focus/input:after:border-primary peer-focus/input:after:border-r-2 peer-focus/input:after:border-t-2',

  'group-data-[invalid]/container:after:border-error group-data-[invalid]/container:after:border-r-2 group-data-[invalid]/container:after:border-t-2',

  'peer-disabled/input:after:border-t-onSurface/12 peer-disabled/input:after:border-r-onSurface/12 peer-disabled/input:peer-placeholder-shown/input:after:border-transparent',
]

const FilledTextFieldRoot = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
>(({ className, error, ...props }, ref) => (
  <div
    ref={ref}
    data-invalid={error ? '' : undefined}
    className={cn(
      'group/container relative z-0 flex w-full items-center rounded-sm bg-surfaceContainer px-2',

      'before:absolute before:inset-0 before:z-[-1] before:rounded-sm before:border-b before:border-onSurfaceVariant/12 before:transition-[color,border]',

      'focus-within:before:border-b-[2px] focus-within:before:border-primary',

      'has-[:disabled]:bg-surfaceContainer/38 has-[:disabled]:before:border-onSurface/12',

      Boolean(error) && 'before:border-error focus-within:before:border-error',
      className
    )}
    {...props}
  />
))
FilledTextFieldRoot.displayName = 'FilledTextFieldRoot'

const OutlinedTextFieldRoot = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
>(({ className, error, ...props }, ref) => (
  <div
    ref={ref}
    data-invalid={error ? '' : undefined}
    className={cn(
      'group/container relative z-0 flex w-full items-center bg-transparent px-2',

      'before:absolute before:inset-0 before:z-[-1] before:rounded-sm before:border before:border-outline before:transition-[color,border]',

      'focus-within:before:border-2 focus-within:before:border-primary',

      'has-[label]:has-[:placeholder-shown]:before:border-outline has-[label]:before:border-t-transparent has-[label]:focus-within:before:border-b-primary has-[label]:focus-within:before:border-t-transparent',

      'has-[:disabled]:before:border-onSurface/12 has-[:disabled]:has-[:placeholder-shown]:before:border-onSurface/12 has-[label]:has-[:disabled]:before:border-transparent has-[:disabled]:before:border-t-transparent has-[label]:has-[:disabled]:before:border-b-onSurface/12 has-[label]:has-[:disabled]:before:border-t-transparent',

      Boolean(error) &&
        'before:border-2 before:border-error has-[label]:has-[:placeholder-shown]:before:border-error has-[label]:focus-within:before:border-b-error',
      className
    )}
    {...props}
  />
))
OutlinedTextFieldRoot.displayName = 'OutlinedTextFieldRoot'

const FilledTextfieldLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(
      commonStyle,
      'left-0 top-0 h-fit w-full pl-4 pt-2 peer-placeholder-shown/input:leading-[2.5] peer-has-[*]/icon:left-9',
      className
    )}
    {...props}
  />
))
FilledTextfieldLabel.displayName = 'FilledTextfieldLabel'

const OutlinedTextfieldLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(commonStyle, outlinedLabel, className)}
    {...props}
  />
))
OutlinedTextfieldLabel.displayName = 'OutlinedTextfieldLabel'

const FilledTextFieldInput = React.forwardRef<
  HTMLInputElement,
  React.InputHTMLAttributes<HTMLInputElement>
>(({ className, placeholder = '', ...props }, ref) => {
  return (
    <input
      ref={ref}
      placeholder={placeholder}
      className={cn(
        'peer/input h-14 grow bg-transparent px-2 text-body-lg text-onSurface caret-primary outline-none transition-opacity placeholder:text-onSurfaceVariant/50 focus:outline-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-onSurface/38 group-has-[label]/container:pt-2 group-has-[:focus]/container:placeholder:opacity-100 group-has-[label]/container:placeholder:opacity-0 group-data-[invalid]/container:caret-error',
        className
      )}
      {...props}
    />
  )
})

FilledTextFieldInput.displayName = 'FilledTextFieldInput'

const OutlinedTextFieldInput = React.forwardRef<
  HTMLInputElement,
  React.InputHTMLAttributes<HTMLInputElement>
>(({ className, placeholder = '', ...props }, ref) => {
  return (
    <input
      ref={ref}
      placeholder={placeholder}
      className={cn(
        'peer/input h-14 grow bg-transparent px-2 text-body-lg text-onSurface caret-primary outline-none transition-opacity placeholder:text-onSurfaceVariant/50 focus:outline-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-onSurface/38 group-has-[label]/container:placeholder:opacity-0 group-has-[label]/container:focus:placeholder:opacity-100 group-data-[invalid]/container:caret-error',
        className
      )}
      {...props}
    />
  )
})

OutlinedTextFieldInput.displayName = 'OutlinedTextFieldInput'

const InputDecoration = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => {
  return (
    <span
      ref={ref}
      className={cn(
        'peer/icon grid h-full shrink-0 place-items-center pl-1 pr-2 text-onSurfaceVariant/70 peer-first-of-type/input:pl-2 peer-first-of-type/input:pr-1 group-has-[:disabled]/container:text-onSurface/38',
        className
      )}
      {...props}
    />
  )
})

InputDecoration.displayName = 'InputDecoration'

const SupportingText = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
  return (
    <p
      ref={ref}
      className={cn('mt-1 px-4 text-body-sm text-onSurfaceVariant', className)}
      {...props}
    />
  )
})
SupportingText.displayName = 'SupportingText'

const FilledTextField = Object.assign(FilledTextFieldRoot, {
  Label: FilledTextfieldLabel,
  Input: FilledTextFieldInput,
  Decoration: InputDecoration,
  SupportingText,
})

const OutlinedTextField = Object.assign(OutlinedTextFieldRoot, {
  Label: OutlinedTextfieldLabel,
  Input: OutlinedTextFieldInput,
  Decoration: InputDecoration,
  SupportingText,
})

export {
  FilledTextField,
  OutlinedTextField,
  FilledTextFieldRoot,
  OutlinedTextFieldRoot,
  FilledTextfieldLabel,
  OutlinedTextfieldLabel,
  FilledTextFieldInput,
  OutlinedTextFieldInput,
  InputDecoration,
  SupportingText,
}

Update the import paths to match your project setup.

Examples

The component has been built with the idea of maintaining modularity. For this reason, it is VERY IMPORTANT to preserve the correct order in the component structure to ensure they work correctly when used.

The component tree is:

<TextField>     Text field root container
  <Decoration>  Text field leading decoration (e.g. icon) (optional)
  <Input />     Text field input
  <Label />     Text field label (optional but recommended)
  <Decoration>  Text field trailing decoration (e.g. icon) (optional)
</TextField>
<SupportingText /> Text field supporting text (optional)

Filled text fields

I’ve chosen a different approach to the border radius than the one suggested in The Material You Guide. I believe this new shape design offers a more modern appearance.

Default

import { FilledTextField } from '@/components/ui/text-field'

export const FilledTextFieldDefault = () => {
  return (
    <FilledTextField>
      <FilledTextField.Input placeholder="First name" />
    </FilledTextField>
  )
}

With label

import { FilledTextField } from '@/components/ui/text-field'

export const FilledTextFieldWithLabel = () => {
  return (
    <FilledTextField>
      <FilledTextField.Input placeholder="Your beautiful first name" />
      <FilledTextField.Label>First name</FilledTextField.Label>
    </FilledTextField>
  )
}

With supporting text

This will be how other users know you.

import { FilledTextField } from '@/components/ui/text-field'

export const FilledTextFieldWithSupportingText = () => {
  return (
    <div className="w-full">
      <FilledTextField>
        <FilledTextField.Input />
        <FilledTextField.Label>Username</FilledTextField.Label>
      </FilledTextField>
      <FilledTextField.SupportingText>
        This will be how other users know you.
      </FilledTextField.SupportingText>
    </div>
  )
}

With leading icon

person
import { Icon } from '@/components/ui/icon'
import { FilledTextField } from '@/components/ui/text-field'

export const FilledTextFieldWithLeadingIcon = () => {
  return (
    <FilledTextField>
      <FilledTextField.Decoration>
        <Icon symbol="person" />
      </FilledTextField.Decoration>
      <FilledTextField.Input />
      <FilledTextField.Label>Username</FilledTextField.Label>
    </FilledTextField>
  )
}

With trailing icon

search
import { Icon } from '@/components/ui/icon'
import { FilledTextField } from '@/components/ui/text-field'

export const FilledTextFieldWithTrailingIcon = () => {
  return (
    <FilledTextField>
      <FilledTextField.Input placeholder="Search..." />
      <FilledTextField.Decoration>
        <Icon symbol="search" />
      </FilledTextField.Decoration>
    </FilledTextField>
  )
}

Disabled

import { FilledTextField } from '@/components/ui/text-field'

export const FilledTextFieldDisabled = () => {
  return (
    <FilledTextField>
      <FilledTextField.Input disabled />
      <FilledTextField.Label>Username</FilledTextField.Label>
    </FilledTextField>
  )
}

With error

The username can not be empty.

import { FilledTextField } from '@/components/ui/text-field'

export const FilledTextFieldWithError = () => {
  return (
    <div className="w-full">
      <FilledTextField error>
        <FilledTextField.Input />
        <FilledTextField.Label>Username</FilledTextField.Label>
      </FilledTextField>
      <FilledTextField.SupportingText className="text-error">
        The username can not be empty.
      </FilledTextField.SupportingText>
    </div>
  )
}

Outlined text fields

Default

import { OutlinedTextField } from '@/components/ui/text-field'

export const OutlinedTextFieldDefault = () => {
  return (
    <OutlinedTextField>
      <OutlinedTextField.Input placeholder="First name" />
    </OutlinedTextField>
  )
}

With label

import { OutlinedTextField } from '@/components/ui/text-field'

export const OutlinedTextFieldWithLabel = () => {
  return (
    <OutlinedTextField>
      <OutlinedTextField.Input placeholder="Your beautiful first name" />
      <OutlinedTextField.Label>First name</OutlinedTextField.Label>
    </OutlinedTextField>
  )
}

With supporting text

This will be how other users know you.

import { OutlinedTextField } from '@/components/ui/text-field'

export const OutlinedTextFieldWithSupportingText = () => {
  return (
    <div className="w-full">
      <OutlinedTextField>
        <OutlinedTextField.Input />
        <OutlinedTextField.Label>Username</OutlinedTextField.Label>
      </OutlinedTextField>
      <OutlinedTextField.SupportingText>
        This will be how other users know you.
      </OutlinedTextField.SupportingText>
    </div>
  )
}

With leading icon

person
import { Icon } from '@/components/ui/icon'
import { OutlinedTextField } from '@/components/ui/text-field'

export const OutlinedTextFieldWithLeadingIcon = () => {
  return (
    <OutlinedTextField>
      <OutlinedTextField.Decoration>
        <Icon symbol="person" />
      </OutlinedTextField.Decoration>
      <OutlinedTextField.Input />
      <OutlinedTextField.Label>Username</OutlinedTextField.Label>
    </OutlinedTextField>
  )
}

With trailing icon

search
import { Icon } from '@/components/ui/icon'
import { OutlinedTextField } from '@/components/ui/text-field'

export const OutlinedTextFieldWithTrailingIcon = () => {
  return (
    <OutlinedTextField>
      <OutlinedTextField.Input placeholder="Search..." />
      <OutlinedTextField.Decoration>
        <Icon symbol="search" />
      </OutlinedTextField.Decoration>
    </OutlinedTextField>
  )
}

Disabled

import { OutlinedTextField } from '@/components/ui/text-field'

export const OutlinedTextFieldDisabled = () => {
  return (
    <OutlinedTextField>
      <OutlinedTextField.Input disabled />
      <OutlinedTextField.Label>Username</OutlinedTextField.Label>
    </OutlinedTextField>
  )
}

With error

The username can not be empty.

import { OutlinedTextField } from '@/components/ui/text-field'

export const OutlinedTextFieldWithError = () => {
  return (
    <div className="w-full">
      <OutlinedTextField error>
        <OutlinedTextField.Input />
        <OutlinedTextField.Label>Username</OutlinedTextField.Label>
      </OutlinedTextField>
      <OutlinedTextField.SupportingText className="text-error">
        The username can not be empty.
      </OutlinedTextField.SupportingText>
    </div>
  )
}