Field
Combine labels, controls, and help text to compose accessible form fields and grouped inputs.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { Checkbox } from '#app/ui/checkbox.tsx'
import {
Field,import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { Checkbox } from '#app/ui/checkbox.tsx'
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from '#app/ui/field.tsx'
import { Input } from '#app/ui/input.tsx'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '#app/ui/select.tsx'
import { Textarea } from '#app/ui/textarea.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
const normalLabel = css({ fontWeight: 400 })
const threeColumns = css({
display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
gap: '1rem',
})
const noResize = css({ resize: 'none' })
export function FieldDemo() {
return () => (
<div mix={wrapperStyles}>
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>All transactions are secure and encrypted</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="checkout-7j9-card-name-43j">Name on Card</FieldLabel>
<Input id="checkout-7j9-card-name-43j" placeholder="Evil Rabbit" required />
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">Card Number</FieldLabel>
<Input id="checkout-7j9-card-number-uw1" placeholder="1234 5678 9012 3456" required />
<FieldDescription>Enter your 16-digit card number</FieldDescription>
</Field>
<div mix={threeColumns}>
<Field>
<FieldLabel htmlFor="checkout-exp-month-ts6">Month</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-exp-month-ts6">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">Year</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-7j9-exp-year-f59">
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-cvv">CVV</FieldLabel>
<Input id="checkout-7j9-cvv" placeholder="123" required />
</Field>
</div>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>The billing address associated with your payment method</FieldDescription>
<FieldGroup>
<Field orientation="horizontal">
<Checkbox id="checkout-7j9-same-as-shipping-wgm" defaultChecked />
<FieldLabel htmlFor="checkout-7j9-same-as-shipping-wgm" mix={normalLabel}>
Same as shipping address
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="checkout-7j9-optional-comments">Comments</FieldLabel>
<Textarea
id="checkout-7j9-optional-comments"
placeholder="Add any additional comments"
mix={noResize}
/>
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button type="submit">Submit</Button>
<Button variant="outline" type="button">
Cancel
</Button>
</Field>
</FieldGroup>
</form>
</div>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/field.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/field.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/field.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/field.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, type CSSMixinDescriptor, type Handle, type Props, type RemixNode } from 'remix/component'
import { Label } from './label.tsx'
import { Separator } from './separator.tsx'
export type FieldOrientation = 'vertical' | 'horizontal' | 'responsive'
export interface FieldSetProps extends Props<'fieldset'> {}
export interface FieldLegendProps extends Props<'legend'> {
variant?: 'legend' | 'label'
}
export interface FieldGroupProps extends Props<'div'> {}
export interface FieldProps extends Props<'div'> {
orientation?: FieldOrientation
}
export interface FieldContentProps extends Props<'div'> {}
export interface FieldLabelProps extends Props<'label'> {}
export interface FieldTitleProps extends Props<'div'> {}
export interface FieldDescriptionProps extends Props<'p'> {}
export interface FieldSeparatorProps extends Props<'div'> {}
export interface FieldErrorProps extends Props<'div'> {
errors?: Array<{ message?: string } | undefined>
}
const fieldSetStyles = css({
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
'&:has(>[data-slot="checkbox-group"])': { gap: '0.75rem' },
'&:has(>[data-slot="radio-group"])': { gap: '0.75rem' },
})
export function FieldSet(handle: Handle<FieldSetProps>) {
return () => {
let { mix, ...props } = handle.props
return <fieldset data-slot="field-set" {...props} mix={[fieldSetStyles, mix]} />
}
}
const fieldLegendStyles = css({
marginBottom: '0.75rem',
fontWeight: 500,
'&[data-variant="legend"]': { fontSize: '1rem', lineHeight: 1.5 },
'&[data-variant="label"]': { fontSize: '0.875rem', lineHeight: 'calc(1.25 / 0.875)' },
})
export function FieldLegend(handle: Handle<FieldLegendProps>) {
return () => {
let { variant = 'legend', mix, ...props } = handle.props
return (
<legend
data-slot="field-legend"
data-variant={variant}
{...props}
mix={[fieldLegendStyles, mix]}
/>
)
}
}
const fieldGroupStyles = css({
containerType: 'inline-size',
containerName: 'field-group',
display: 'flex',
width: '100%',
flexDirection: 'column',
gap: '1.75rem',
'&[data-slot="checkbox-group"]': { gap: '0.75rem' },
'& > [data-slot="field-group"]': { gap: '1rem' },
})
export function FieldGroup(handle: Handle<FieldGroupProps>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="field-group" {...props} mix={[fieldGroupStyles, mix]} />
}
}
const fieldBase = css({
display: 'flex',
width: '100%',
gap: '0.75rem',
'&[data-invalid="true"]': { color: 'var(--destructive)' },
})
/** `@md/field-group:` — the nearest `FieldGroup` container is at least 28rem wide. */
const md = '@container field-group (min-width: 28rem)'
/*
* Tailwind's `[&>*]:w-full` beats the children's own width utilities (`w-fit` on FieldLabel,
* SelectTrigger, …). Each child's styles live in a later cascade layer here, so the child rules
* are `!important` to keep shadcn's precedence.
*/
const childFullWidth = {
'& > *': { width: '100% !important' },
'& > .sr-only': { width: 'auto !important' },
}
const fieldOrientations: Record<FieldOrientation, CSSMixinDescriptor> = {
vertical: css({
flexDirection: 'column',
...childFullWidth,
}),
horizontal: css({
flexDirection: 'row',
alignItems: 'center',
'& > [data-slot="field-label"]': { flex: '1 1 auto' },
'&:has(>[data-slot="field-content"])': { alignItems: 'flex-start' },
'&:has(>[data-slot="field-content"]) > :is([role="checkbox"], [role="radio"])': {
marginTop: '1px',
},
}),
responsive: css({
flexDirection: 'column',
...childFullWidth,
[md]: {
flexDirection: 'row',
alignItems: 'center',
'& > *': { width: 'auto !important' },
'& > [data-slot="field-label"]': { flex: '1 1 auto' },
'&:has(>[data-slot="field-content"])': { alignItems: 'flex-start' },
'&:has(>[data-slot="field-content"]) > :is([role="checkbox"], [role="radio"])': {
marginTop: '1px',
},
},
}),
}
/** The `fieldVariants()` of shadcn/ui, as a mix. */
export function fieldVariants({ orientation = 'vertical' }: { orientation?: FieldOrientation } = {}) {
return [fieldBase, fieldOrientations[orientation]]
}
export function Field(handle: Handle<FieldProps>) {
return () => {
let { orientation = 'vertical', mix, ...props } = handle.props
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
{...props}
mix={[fieldVariants({ orientation }), mix]}
/>
)
}
}
const fieldContentStyles = css({
display: 'flex',
flex: '1 1 0%',
flexDirection: 'column',
gap: '0.375rem',
lineHeight: 1.375,
})
export function FieldContent(handle: Handle<FieldContentProps>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="field-content" {...props} mix={[fieldContentStyles, mix]} />
}
}
const fieldLabelStyles = css({
display: 'flex',
width: 'fit-content',
gap: '0.5rem',
lineHeight: 1.375,
'[data-slot="field"][data-disabled="true"] &': { opacity: 0.5 },
'&:has(>[data-slot="field"])': {
width: '100%',
flexDirection: 'column',
borderRadius: 'calc(var(--radius) - 2px)',
borderWidth: '1px',
},
'& > [data-slot="field"]': { padding: '1rem' },
'&:has([data-state="checked"])': {
borderColor: 'var(--primary)',
backgroundColor: 'color-mix(in oklab, var(--primary) 5%, transparent)',
},
'.dark &:has([data-state="checked"])': {
backgroundColor: 'color-mix(in oklab, var(--primary) 10%, transparent)',
},
})
export function FieldLabel(handle: Handle<FieldLabelProps>) {
return () => {
let { mix, ...props } = handle.props
return <Label data-slot="field-label" {...props} mix={[fieldLabelStyles, mix]} />
}
}
const fieldTitleStyles = css({
display: 'flex',
width: 'fit-content',
alignItems: 'center',
gap: '0.5rem',
fontSize: '0.875rem',
lineHeight: 1.375,
fontWeight: 500,
'[data-slot="field"][data-disabled="true"] &': { opacity: 0.5 },
})
export function FieldTitle(handle: Handle<FieldTitleProps>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="field-label" {...props} mix={[fieldTitleStyles, mix]} />
}
}
const fieldDescriptionStyles = css({
fontSize: '0.875rem',
lineHeight: 1.5,
fontWeight: 400,
color: 'var(--muted-foreground)',
'[data-slot="field"]:has([data-orientation="horizontal"]) &': { textWrap: 'balance' },
'&:last-child': { marginTop: 0 },
'&:nth-last-child(2)': { marginTop: '-0.25rem' },
'[data-variant="legend"] + &': { marginTop: '-0.375rem' },
'& > a': { textDecorationLine: 'underline', textUnderlineOffset: '4px' },
'& > a:hover': { color: 'var(--primary)' },
})
export function FieldDescription(handle: Handle<FieldDescriptionProps>) {
return () => {
let { mix, ...props } = handle.props
return <p data-slot="field-description" {...props} mix={[fieldDescriptionStyles, mix]} />
}
}
const fieldSeparatorStyles = css({
position: 'relative',
marginBlock: '-0.5rem',
height: '1.25rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
'[data-slot="field-group"][data-variant="outline"] &': { marginBottom: '-0.5rem' },
})
const fieldSeparatorLineStyles = css({ position: 'absolute', inset: 0, top: '50%' })
const fieldSeparatorContentStyles = css({
position: 'relative',
marginInline: 'auto',
display: 'block',
width: 'fit-content',
backgroundColor: 'var(--background)',
paddingInline: '0.5rem',
color: 'var(--muted-foreground)',
})
export function FieldSeparator(handle: Handle<FieldSeparatorProps>) {
return () => {
let { children, mix, ...props } = handle.props
let hasContent = children != null && children !== false && children !== ''
return (
<div
data-slot="field-separator"
data-content={hasContent}
{...props}
mix={[fieldSeparatorStyles, mix]}
>
<Separator mix={fieldSeparatorLineStyles} />
{hasContent && (
<span data-slot="field-separator-content" mix={fieldSeparatorContentStyles}>
{children}
</span>
)}
</div>
)
}
}
const fieldErrorStyles = css({
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
fontWeight: 400,
color: 'var(--destructive)',
})
const fieldErrorListStyles = css({
marginLeft: '1rem',
display: 'flex',
listStyleType: 'disc',
flexDirection: 'column',
gap: '0.25rem',
})
export function FieldError(handle: Handle<FieldErrorProps>) {
return () => {
let { children, errors, mix, ...props } = handle.props
let content: RemixNode = children
if (!children) {
let uniqueErrors = [...new Map(errors?.map((error) => [error?.message, error])).values()]
if (uniqueErrors.length === 1) {
content = uniqueErrors[0]?.message
} else if (uniqueErrors.length > 1) {
content = (
<ul mix={fieldErrorListStyles}>
{uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
</ul>
)
}
}
if (!content) return null
return (
<div role="alert" data-slot="field-error" {...props} mix={[fieldErrorStyles, mix]}>
{content}
</div>
)
}
}Update the import paths to match your project setup.
Usage
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from '#app/ui/field.tsx'<FieldSet>
<FieldLegend>Profile</FieldLegend>
<FieldDescription>This appears on invoices and emails.</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Full name</FieldLabel>
<Input id="name" autoComplete="off" placeholder="Evil Rabbit" />
<FieldDescription>This appears on invoices and emails.</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" autoComplete="off" aria-invalid />
<FieldError>Choose another username.</FieldError>
</Field>
<Field orientation="horizontal">
<Switch id="newsletter" />
<FieldLabel htmlFor="newsletter">Subscribe to the newsletter</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>Examples
Input
import { css } from 'remix/component'
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from '#app/ui/field.tsx'
import { Input } from '#app/ui/input.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })import { css } from 'remix/component'
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from '#app/ui/field.tsx'
import { Input } from '#app/ui/input.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
export function FieldInput() {
return () => (
<div mix={wrapperStyles}>
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" type="text" placeholder="Max Leiter" />
<FieldDescription>Choose a unique username for your account.</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<FieldDescription>Must be at least 8 characters long.</FieldDescription>
<Input id="password" type="password" placeholder="••••••••" />
</Field>
</FieldGroup>
</FieldSet>
</div>
)
}Textarea
import { css } from 'remix/component'
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from '#app/ui/field.tsx'
import { Textarea } from '#app/ui/textarea.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })import { css } from 'remix/component'
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from '#app/ui/field.tsx'
import { Textarea } from '#app/ui/textarea.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
export function FieldTextarea() {
return () => (
<div mix={wrapperStyles}>
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="feedback">Feedback</FieldLabel>
<Textarea id="feedback" placeholder="Your feedback helps us improve..." rows={4} />
<FieldDescription>Share your thoughts about our service.</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>
</div>
)
}Select
Select your department or area of work.
import { css } from 'remix/component'
import { Field, FieldDescription, FieldLabel } from '#app/ui/field.tsx'
import {
Select,
SelectContent,import { css } from 'remix/component'
import { Field, FieldDescription, FieldLabel } from '#app/ui/field.tsx'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '#app/ui/select.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
export function FieldSelect() {
return () => (
<div mix={wrapperStyles}>
<Field>
<FieldLabel>Department</FieldLabel>
<Select>
<SelectTrigger>
<SelectValue placeholder="Choose department" />
</SelectTrigger>
<SelectContent>
<SelectItem value="engineering">Engineering</SelectItem>
<SelectItem value="design">Design</SelectItem>
<SelectItem value="marketing">Marketing</SelectItem>
<SelectItem value="sales">Sales</SelectItem>
<SelectItem value="support">Customer Support</SelectItem>
<SelectItem value="hr">Human Resources</SelectItem>
<SelectItem value="finance">Finance</SelectItem>
<SelectItem value="operations">Operations</SelectItem>
</SelectContent>
</Select>
<FieldDescription>Select your department or area of work.</FieldDescription>
</Field>
</div>
)
}Fieldset
import { css } from 'remix/component'
import {
Field,
FieldDescription,
FieldGroup,import { css } from 'remix/component'
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from '#app/ui/field.tsx'
import { Input } from '#app/ui/input.tsx'
const wrapperStyles = css({
width: '100%',
maxWidth: '28rem',
'& > :not(:last-child)': { marginBottom: '1.5rem' },
})
const twoColumns = css({
display: 'grid',
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
gap: '1rem',
})
export function FieldFieldset() {
return () => (
<div mix={wrapperStyles}>
<FieldSet>
<FieldLegend>Address Information</FieldLegend>
<FieldDescription>We need your address to deliver your order.</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="street">Street Address</FieldLabel>
<Input id="street" type="text" placeholder="123 Main St" />
</Field>
<div mix={twoColumns}>
<Field>
<FieldLabel htmlFor="city">City</FieldLabel>
<Input id="city" type="text" placeholder="New York" />
</Field>
<Field>
<FieldLabel htmlFor="zip">Postal Code</FieldLabel>
<Input id="zip" type="text" placeholder="90502" />
</Field>
</div>
</FieldGroup>
</FieldSet>
</div>
)
}Checkbox
styleName="radix-nova"
Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.
import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
import {
Field,
FieldContent,import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from '#app/ui/field.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
const normalLabel = css({ fontWeight: 400 })
const tightGroup = css({ gap: '0.75rem' })
export function FieldCheckbox() {
return () => (
<div mix={wrapperStyles}>
<FieldGroup>
<FieldSet>
<FieldLegend variant="label">Show these items on the desktop</FieldLegend>
<FieldDescription>Select the items you want to show on the desktop.</FieldDescription>
<FieldGroup mix={tightGroup}>
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-hard-disks-ljj" />
<FieldLabel htmlFor="finder-pref-9k2-hard-disks-ljj" mix={normalLabel}>
Hard disks
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-external-disks-1yg" />
<FieldLabel htmlFor="finder-pref-9k2-external-disks-1yg" mix={normalLabel}>
External disks
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-cds-dvds-fzt" />
<FieldLabel htmlFor="finder-pref-9k2-cds-dvds-fzt" mix={normalLabel}>
CDs, DVDs, and iPods
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-connected-servers-6l2" />
<FieldLabel htmlFor="finder-pref-9k2-connected-servers-6l2" mix={normalLabel}>
Connected servers
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<Checkbox id="finder-pref-9k2-sync-folders-nep" defaultChecked />
<FieldContent>
<FieldLabel htmlFor="finder-pref-9k2-sync-folders-nep">
Sync Desktop & Documents folders
</FieldLabel>
<FieldDescription>
Your Desktop & Documents folders are being synced with iCloud Drive. You can access
them from other devices.
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
</div>
)
}Radio
import { css } from 'remix/component'
import { Field, FieldDescription, FieldLabel, FieldSet } from '#app/ui/field.tsx'
import { RadioGroup, RadioGroupItem } from '#app/ui/radio-group.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })import { css } from 'remix/component'
import { Field, FieldDescription, FieldLabel, FieldSet } from '#app/ui/field.tsx'
import { RadioGroup, RadioGroupItem } from '#app/ui/radio-group.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
const normalLabel = css({ fontWeight: 400 })
export function FieldRadio() {
return () => (
<div mix={wrapperStyles}>
<FieldSet>
<FieldLabel>Subscription Plan</FieldLabel>
<FieldDescription>Yearly and lifetime plans offer significant savings.</FieldDescription>
<RadioGroup defaultValue="monthly">
<Field orientation="horizontal">
<RadioGroupItem value="monthly" id="plan-monthly" />
<FieldLabel htmlFor="plan-monthly" mix={normalLabel}>
Monthly ($9.99/month)
</FieldLabel>
</Field>
<Field orientation="horizontal">
<RadioGroupItem value="yearly" id="plan-yearly" />
<FieldLabel htmlFor="plan-yearly" mix={normalLabel}>
Yearly ($99.99/year)
</FieldLabel>
</Field>
<Field orientation="horizontal">
<RadioGroupItem value="lifetime" id="plan-lifetime" />
<FieldLabel htmlFor="plan-lifetime" mix={normalLabel}>
Lifetime ($299.99)
</FieldLabel>
</Field>
</RadioGroup>
</FieldSet>
</div>
)
}Switch
Enable multi-factor authentication. If you do not have a two-factor device, you can use a one-time code sent to your email.
import { css } from 'remix/component'
import { Field, FieldContent, FieldDescription, FieldLabel } from '#app/ui/field.tsx'
import { Switch } from '#app/ui/switch.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })import { css } from 'remix/component'
import { Field, FieldContent, FieldDescription, FieldLabel } from '#app/ui/field.tsx'
import { Switch } from '#app/ui/switch.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
export function FieldSwitch() {
return () => (
<div mix={wrapperStyles}>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="2fa">Multi-factor authentication</FieldLabel>
<FieldDescription>
Enable multi-factor authentication. If you do not have a two-factor device, you can use
a one-time code sent to your email.
</FieldDescription>
</FieldContent>
<Switch id="2fa" />
</Field>
</div>
)
}Choice Card
Wrap Field components inside FieldLabel to create selectable field groups. This works with RadioItem, Checkbox and Switch components.
import { css } from 'remix/component'
import {
Field,
FieldContent,
FieldDescription,import { css } from 'remix/component'
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSet,
FieldTitle,
} from '#app/ui/field.tsx'
import { RadioGroup, RadioGroupItem } from '#app/ui/radio-group.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
export function FieldChoiceCard() {
return () => (
<div mix={wrapperStyles}>
<FieldGroup>
<FieldSet>
<FieldLabel htmlFor="compute-environment-p8w">Compute Environment</FieldLabel>
<FieldDescription>Select the compute environment for your cluster.</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="kubernetes-r2h">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>Run GPU workloads on a K8s configured cluster.</FieldDescription>
</FieldContent>
<RadioGroupItem value="kubernetes" id="kubernetes-r2h" />
</Field>
</FieldLabel>
<FieldLabel htmlFor="vm-z4k">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a VM configured cluster to run GPU workloads.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="vm" id="vm-z4k" />
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
</FieldGroup>
</div>
)
}Field Group
Stack Field components with FieldGroup. Add FieldSeparator to divide them.
import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
import {
Field,
FieldDescription,import { css } from 'remix/component'
import { Checkbox } from '#app/ui/checkbox.tsx'
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSeparator,
FieldSet,
} from '#app/ui/field.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '28rem' })
const normalLabel = css({ fontWeight: 400 })
export function FieldGroupExample() {
return () => (
<div mix={wrapperStyles}>
<FieldGroup>
<FieldSet>
<FieldLabel>Responses</FieldLabel>
<FieldDescription>
Get notified when ChatGPT responds to requests that take time, like research or image
generation.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field orientation="horizontal">
<Checkbox id="push" defaultChecked disabled />
<FieldLabel htmlFor="push" mix={normalLabel}>
Push notifications
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLabel>Tasks</FieldLabel>
<FieldDescription>
Get notified when tasks you've created have updates. <a href="#">Manage tasks</a>
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
<Field orientation="horizontal">
<Checkbox id="push-tasks" />
<FieldLabel htmlFor="push-tasks" mix={normalLabel}>
Push notifications
</FieldLabel>
</Field>
<Field orientation="horizontal">
<Checkbox id="email-tasks" />
<FieldLabel htmlFor="email-tasks" mix={normalLabel}>
Email notifications
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
</FieldGroup>
</div>
)
}Responsive Layout
- **Vertical fields:** Default orientation stacks label, control, and helper text—ideal for mobile-first layouts. - **Horizontal fields:** Set orientation="horizontal" on Field to align the label and control side-by-side. Pair with FieldContent to keep descriptions aligned. - **Responsive fields:** Set orientation="responsive" for automatic column layouts inside container-aware parents. Apply @container/field-group classes on FieldGroup to switch orientations at specific breakpoints.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import {
Field,
FieldContent,import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from '#app/ui/field.tsx'
import { Input } from '#app/ui/input.tsx'
import { Textarea } from '#app/ui/textarea.tsx'
const wrapperStyles = css({ width: '100%', maxWidth: '56rem' })
const messageStyles = css({
minHeight: '100px',
resize: 'none',
'@media (min-width: 40rem)': { minWidth: '300px' },
})
export function FieldResponsive() {
return () => (
<div mix={wrapperStyles}>
<form>
<FieldSet>
<FieldLegend>Profile</FieldLegend>
<FieldDescription>Fill in your profile information.</FieldDescription>
<FieldSeparator />
<FieldGroup>
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="name">Name</FieldLabel>
<FieldDescription>Provide your full name for identification</FieldDescription>
</FieldContent>
<Input id="name" placeholder="Evil Rabbit" required />
</Field>
<FieldSeparator />
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="lastName">Message</FieldLabel>
<FieldDescription>
You can write your message here. Keep it short, preferably under 100 characters.
</FieldDescription>
</FieldContent>
<Textarea id="message" placeholder="Hello, world!" required mix={messageStyles} />
</Field>
<FieldSeparator />
<Field orientation="responsive">
<Button type="submit">Submit</Button>
<Button type="button" variant="outline">
Cancel
</Button>
</Field>
</FieldGroup>
</FieldSet>
</form>
</div>
)
}API Reference
FieldSet
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
FieldLegend
| Prop | Type | Default |
|---|---|---|
variant | "legend" | "label" | "legend" |
mix | MixValue | - |
FieldGroup
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
Field
| Prop | Type | Default |
|---|---|---|
orientation | FieldOrientation | "vertical" |
mix | MixValue | - |
FieldContent
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
FieldLabel
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
FieldTitle
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
FieldDescription
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
FieldSeparator
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
FieldError
| Prop | Type | Default |
|---|---|---|
errors | ({ message?: string | undefined; } | undefined)[] | - |
mix | MixValue | - |