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