Components
Text field
An input for single-line text data. Basic text fields with labels, field descriptions, and validation states.
field-sizing.
Full support Supported since v26.2. Variants
---import { TextField } from "@opui/astro"---
<TextField label="Outlined" placeholder="Placeholder" /><TextField label="Filled" placeholder="Placeholder" filled /><label class="text-field"> <span class="label">Outlined</span> <span class="field"> <input id="text-field-2" placeholder="Placeholder" type="text" /> </span></label><label class="text-field filled"> <span class="label">Filled</span> <span class="field"> <input id="text-field-3" placeholder="Placeholder" type="text" /> </span></label>Sizes
---import { TextField } from "@opui/astro"---
<TextField label="Small outlined" placeholder="Placeholder" small /><TextField label="Small filled" placeholder="Placeholder" small filled /><label class="text-field small"> <span class="label">Small outlined</span> <span class="field"> <input id="text-field-4" placeholder="Placeholder" type="text" /> </span></label><label class="text-field filled small"> <span class="label">Small filled</span> <span class="field"> <input id="text-field-5" placeholder="Placeholder" type="text" /> </span></label>End text
---import { TextField } from "@opui/astro"---
<TextField label="Label" placeholder="Outlined" endText="Supporting text" /><label class="text-field"> <span class="label">Label</span> <span class="field"> <input id="text-field-6" placeholder="Outlined" type="text" /> </span> <span class="end-text">Supporting text</span></label>Affix
Use the prefix, suffix, header,
and footer slots to affix content inside the field's border.
Prefix and suffix sit beside the input, while header and footer span the field's
full width with a divider.
---import { TextField } from "@opui/astro"---
<TextField label="Amount" placeholder="0.00"> <Fragment slot="prefix">¢</Fragment> <Fragment slot="suffix">EUR</Fragment></TextField>
<TextField label="Website" placeholder="example.com"> <Fragment slot="prefix">https://</Fragment></TextField>
<TextField label="Weight" type="numeric" placeholder="0"> <Fragment slot="suffix">kg</Fragment></TextField>
<TextField label="Search" placeholder="Search..."> <svg slot="prefix" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.3-4.3"></path> </svg></TextField><label class="text-field"> <span class="label">Amount</span> <span class="field"> <input id="text-field-7" placeholder="0.00" type="text" /> <span class="prefix">¢</span> <span class="suffix">EUR</span> </span></label><label class="text-field"> <span class="label">Website</span> <span class="field"> <input id="text-field-8" placeholder="example.com" type="text" /> <span class="prefix">https://</span> </span></label><label class="text-field"> <span class="label">Weight</span> <span class="field"> <input id="text-field-9" inputmode="numeric" pattern="[0-9]*" placeholder="0" type="text" /> <span class="suffix">kg</span> </span></label><label class="text-field"> <span class="label">Search</span> <span class="field"> <input id="text-field-10" placeholder="Search..." type="text" /> <span class="prefix" ><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.3-4.3"></path></svg ></span> </span></label>Headers and footers
Use the header slot for inside-field captions (filenames, categories)
and the footer slot for counters, hints, or action buttons.
---import { TextField } from "@opui/astro"---
<TextField label="Username" placeholder="Enter your name"> <Fragment slot="header">Full Name</Fragment></TextField>
<TextField label="Tagline" placeholder="A short description"> <Fragment slot="footer">0 / 80</Fragment></TextField><label class="text-field"> <span class="label">Username</span> <span class="field"> <input id="text-field-11" placeholder="Enter your name" type="text" /> <span class="header">Full Name</span> </span></label><label class="text-field"> <span class="label">Tagline</span> <span class="field"> <input id="text-field-12" placeholder="A short description" type="text" /> <span class="footer">0 / 80</span> </span></label>Validation
Add [required] to the <input> element to toggle
required styles
The .critical class toggles the error styles. Make use of the end
text to give extra feedback on the error.
---import { TextField } from "@opui/astro"---
<div class="example-row"> <TextField label="I'm required" placeholder="Placeholder" required /> <TextField label="So am I!" placeholder="Placeholder" required filled /></div>
<div class="example-row"> <TextField label="Label" placeholder="Placeholder" value="This isn't right" endText="Only double-negatives are allowed." critical /> <TextField label="Label" placeholder="Placeholder" value="Uh-oh" endText="Only letters from the first half of the alphabet are allowed." critical filled /></div><div class="example-row"> <label class="text-field"> <span class="label">I'm required</span> <span class="field"> <input id="text-field-13" placeholder="Placeholder" required type="text" /> </span> </label> <label class="text-field filled"> <span class="label">So am I!</span> <span class="field"> <input id="text-field-14" placeholder="Placeholder" required type="text" /> </span> </label></div><div class="example-row"> <label class="text-field" data-invalid="true"> <span class="label">Label</span> <span class="field"> <input id="text-field-15" placeholder="Placeholder" type="text" value="This isn't right" /> </span> <span class="end-text">Only double-negatives are allowed.</span> </label> <label class="text-field filled" data-invalid="true"> <span class="label">Label</span> <span class="field"> <input id="text-field-16" placeholder="Placeholder" type="text" value="Uh-oh" /> </span> <span class="end-text" >Only letters from the first half of the alphabet are allowed.</span > </label></div>Spread
Use the spread boolean prop to display the label and description
on the left with the input on the right. The layout collapses to a column
on narrow containers.
---import { TextField } from "@opui/astro"---
<TextField spread placeholder="Evil Rabbit"> <Fragment slot="label">Name</Fragment> <Fragment slot="description" >Provide your full name for identification</Fragment ></TextField>
<TextField spread placeholder="you@example.com" type="email" filled> <Fragment slot="label">Email</Fragment> <Fragment slot="description">We'll use this to contact you</Fragment> <Fragment slot="end-text">Please use a valid email address</Fragment></TextField>
<TextField spread required label="Required"> <Fragment slot="description">You must fill this in</Fragment></TextField>
<TextField spread disabled label="Disabled"> <Fragment slot="description">This field is disabled</Fragment></TextField>
<TextField spread critical label="Invalid Name"> <Fragment slot="description">This field has an error</Fragment> <Fragment slot="end-text">This value is too short.</Fragment></TextField>
<TextField spread label="Amount" placeholder="0.00"> <Fragment slot="description">Daily spending limit</Fragment> <Fragment slot="prefix">¢</Fragment> <Fragment slot="suffix">EUR</Fragment></TextField>
<TextField spread label="Website" placeholder="example.com" filled> <Fragment slot="description">Your public profile URL</Fragment> <Fragment slot="prefix">https://</Fragment> <Fragment slot="end-text">Must include a valid domain</Fragment></TextField>
<TextField spread label="Project name" placeholder="my-project"> <Fragment slot="description">Used to generate the project URL</Fragment> <Fragment slot="header">acme.dev/</Fragment> <Fragment slot="footer">Lowercase letters and dashes only</Fragment></TextField>
<TextField spread filled label="API key" placeholder="Paste your key" type="password"> <Fragment slot="description">Stored encrypted at rest</Fragment> <Fragment slot="prefix"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </Fragment> <Fragment slot="header">Secret</Fragment> <Fragment slot="footer">Rotates every 90 days</Fragment> <Fragment slot="end-text">Treat like a password</Fragment></TextField><label class="text-field spread"> <span class="label">Name</span> <span class="start-text">Provide your full name for identification</span> <span class="field"> <input id="text-field-17" placeholder="Evil Rabbit" type="text" /> </span></label><label class="text-field filled spread"> <span class="label">Email</span> <span class="start-text">We'll use this to contact you</span> <span class="field"> <input id="text-field-18" placeholder="you@example.com" type="email" /> </span> <span class="end-text">Please use a valid email address</span></label><label class="text-field spread"> <span class="label">Required</span> <span class="start-text">You must fill this in</span> <span class="field"> <input id="text-field-19" required type="text" /> </span></label><label class="text-field spread"> <span class="label">Disabled</span> <span class="start-text">This field is disabled</span> <span class="field"> <input disabled id="text-field-20" type="text" /> </span></label><label class="text-field spread" data-invalid="true"> <span class="label">Invalid Name</span> <span class="start-text">This field has an error</span> <span class="field"> <input id="text-field-21" type="text" /> </span> <span class="end-text">This value is too short.</span></label><label class="text-field spread"> <span class="label">Amount</span> <span class="start-text">Daily spending limit</span> <span class="field"> <input id="text-field-22" placeholder="0.00" type="text" /> <span class="prefix">¢</span> <span class="suffix">EUR</span> </span></label><label class="text-field filled spread"> <span class="label">Website</span> <span class="start-text">Your public profile URL</span> <span class="field"> <input id="text-field-23" placeholder="example.com" type="text" /> <span class="prefix">https://</span> </span> <span class="end-text">Must include a valid domain</span></label><label class="text-field spread"> <span class="label">Project name</span> <span class="start-text">Used to generate the project URL</span> <span class="field"> <input id="text-field-24" placeholder="my-project" type="text" /> <span class="header">acme.dev/</span> <span class="footer">Lowercase letters and dashes only</span> </span></label><label class="text-field filled spread"> <span class="label">API key</span> <span class="start-text">Stored encrypted at rest</span> <span class="field"> <input id="text-field-25" placeholder="Paste your key" type="password" /> <span class="prefix"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </span> <span class="header">Secret</span> <span class="footer">Rotates every 90 days</span> </span> <span class="end-text">Treat like a password</span></label>Auto-fit
When enabled the Field changes size depending on its content.
---import { TextField } from "@opui/astro"---
<TextField label="Label" placeholder="Auto-fit" autoFit /><label class="text-field auto-fit"> <span class="label">Label</span> <span class="field"> <input id="text-field-26" placeholder="Auto-fit" type="text" /> </span></label>Input types
<div class="example-column"> <label class="text-field input-type-field"> <span class="label">Color</span> <input type="color" placeholder="Color" /> </label> <label class="text-field input-type-field"> <span class="label">Email</span> <input type="email" placeholder="name@email.com" /> </label> <label class="text-field input-type-field"> <span class="label">Password</span> <input type="password" placeholder="Password" /> </label> <label class="text-field input-type-field"> <span class="label">Search</span> <input type="search" placeholder="Search" /> </label> <label class="text-field input-type-field"> <span class="label">Phone</span> <input type="tel" placeholder="(666) 666-1337" /> </label> <label class="text-field input-type-field"> <span class="label">Text</span> <input type="text" placeholder="Text" /> </label> <label class="text-field input-type-field"> <span class="label">URL</span> <input type="url" placeholder="https://yoursite.com" /> </label></div>
<div class="example-column"> <label class="text-field input-type-field"> <span class="label">Date</span> <input type="date" placeholder="Date" /> </label> <label class="text-field input-type-field"> <span class="label">Datetime local</span> <input type="datetime-local" placeholder="Datetime local" /> </label> <label class="text-field input-type-field"> <span class="label">Month</span> <input type="month" placeholder="Month" /> </label> <label class="text-field input-type-field"> <span class="label">Time</span> <input type="time" placeholder="Time" /> </label> <label class="text-field input-type-field"> <span class="label">Week</span> <input type="week" placeholder="Week" /> </label></div>
<script> function setupInputTypesControls() { const filledToggle = document.querySelector( "#text-field-filled-toggle", ) as HTMLInputElement const smallToggle = document.querySelector( "#text-field-small-toggle", ) as HTMLInputElement const fields = document.querySelectorAll(".input-type-field")
function updateFields() { if (!filledToggle || !smallToggle) return fields.forEach((field) => { field.classList.toggle("filled", filledToggle.checked) field.classList.toggle("small", smallToggle.checked) }) }
filledToggle?.addEventListener("change", updateFields) smallToggle?.addEventListener("change", updateFields) }
setupInputTypesControls() document.addEventListener("astro:after-swap", setupInputTypesControls)</script><script type="module"> function o() { const e = document.querySelector("#text-field-filled-toggle"), t = document.querySelector("#text-field-small-toggle"), s = document.querySelectorAll(".input-type-field"); function l() { !e || !t || s.forEach((n) => { (n.classList.toggle("filled", e.checked), n.classList.toggle("small", t.checked)); }); } (e?.addEventListener("change", l), t?.addEventListener("change", l)); } o(); document.addEventListener("astro:after-swap", o);</script><div class="example-column"> <label class="text-field input-type-field"> <span class="label">Color</span> <input type="color" placeholder="Color" /> </label> <label class="text-field input-type-field"> <span class="label">Email</span> <input type="email" placeholder="name@email.com" /> </label> <label class="text-field input-type-field"> <span class="label">Password</span> <input type="password" placeholder="Password" /> </label> <label class="text-field input-type-field"> <span class="label">Search</span> <input type="search" placeholder="Search" /> </label> <label class="text-field input-type-field"> <span class="label">Phone</span> <input type="tel" placeholder="(666) 666-1337" /> </label> <label class="text-field input-type-field"> <span class="label">Text</span> <input type="text" placeholder="Text" /> </label> <label class="text-field input-type-field"> <span class="label">URL</span> <input type="url" placeholder="https://yoursite.com" /> </label></div><div class="example-column"> <label class="text-field input-type-field"> <span class="label">Date</span> <input type="date" placeholder="Date" /> </label> <label class="text-field input-type-field"> <span class="label">Datetime local</span> <input type="datetime-local" placeholder="Datetime local" /> </label> <label class="text-field input-type-field"> <span class="label">Month</span> <input type="month" placeholder="Month" /> </label> <label class="text-field input-type-field"> <span class="label">Time</span> <input type="time" placeholder="Time" /> </label> <label class="text-field input-type-field"> <span class="label">Week</span> <input type="week" placeholder="Week" /> </label></div>Date inputs
Numeric vs <input type="number">
---import { TextField } from "@opui/astro"---
<TextField label="Numeric" placeholder="Numeric" type="numeric" /><label class="text-field"> <span class="label">Numeric</span> <span class="field"> <input id="text-field-27" inputmode="numeric" pattern="[0-9]*" placeholder="Numeric" type="text" /> </span></label>You most likely don't need <input type="number">
While <input type="number"> may seem logical for numeric
data it should only be used when mathematical operations are needed on
the input (which is... never). Data like credit card numbers, IDs or social
security numbers - are actually text that happen to be numeric rather
than mathematical values. Therefore, consider using
<input type="text" inputmode="numeric" pattern="[0-9]*"> instead.
You will have a bad time.
This triggers the numeric keyboard on mobile devices while avoiding the jank of number inputs, such as:
- Unexpected value increments from scroll wheels
- Browser-specific validation differences
- Accessibility problems
- Removal of leading zeros
- Allows for some non-numeric mathematical characters
It should probably be called <input type="math"> instead.
The British Government has a great article about how bad input number is and goes in-depth. It's a very interesting read.
File
Use aria-label instead of the <label> element.
File is a weird one. Should it really be an <input> element?
Well, it's what we've got :sweat_smile:
---import { TextField } from "@opui/astro"---
<TextField type="file" placeholder="File" label="Label" /><TextField type="file" placeholder="File" label="Label" filled /><label class="text-field"> <span class="label">Label</span> <span class="field"> <input id="text-field-28" placeholder="File" type="file" /> </span></label><label class="text-field filled"> <span class="label">Label</span> <span class="field"> <input id="text-field-29" placeholder="File" type="file" /> </span></label>Autosuggest
Leverages the <input> + <datalist> element
combo.
<input list="DATALISTID"><datalist id="DATALISTID">
---import { TextField } from "@opui/astro"---
<TextField label="Users" list="users" placeholder="Placeholder"> <datalist id="users"> <option value="Ray Manzarek"></option> <option value="Jonny Greenwood"></option> <option value="Marika Hackman"></option> </datalist></TextField>
<TextField filled label="Emails" list="users-email" placeholder="Placeholder" type="email"> <datalist id="users-email"> <option value="ray.manzarek@the.doors"></option> <option value="jonny.greenwood@radio.head"></option> <option value="marika@hack.man"></option> </datalist></TextField><label class="text-field"> <span class="label">Users</span> <span class="field"> <input id="text-field-30" list="users" placeholder="Placeholder" type="text" /> </span> <datalist id="users"> <option value="Ray Manzarek"></option> <option value="Jonny Greenwood"></option> <option value="Marika Hackman"></option> </datalist></label><label class="text-field filled"> <span class="label">Emails</span> <span class="field"> <input id="text-field-31" list="users-email" placeholder="Placeholder" type="email" /> </span> <datalist id="users-email"> <option value="ray.manzarek@the.doors"></option> <option value="jonny.greenwood@radio.head"></option> <option value="marika@hack.man"></option> </datalist></label>
Think of <datalist> as a list of suggested values.
-
<select>only allows you to choose between its provided values. -
<input>lets you input anything you want. -
<input>+<datalist>is a hybrid between the two.
Do I have to use <label>?
No. But you get some accessibility wins for free with <label>. It's recommended to label your inputs somehow.
<div class="text-field"> <input type="text" placeholder="Placeholder" /></div><div class="text-field"><input type="text" placeholder="Placeholder" /></div>Accessibility
- Don't use
<input type="number">unless your user research tells you to.
Anatomy
label.text-field: Container element.label: Field label element.field: The boxed input area-
.header: Optional inside-border header strip (with divider) .prefix: Optional inline-start affix<input>: Input element.suffix: Optional inline-end affix-
.footer: Optional inside-border footer strip (with divider) .end-text: Supporting text element
API
Text field API
| Prop | Type | Default | Description |
|---|---|---|---|
autoFit | boolean | false | Automatically adjusts its width to content. |
description | string | - | Description text displayed above the field. |
critical | boolean | false | Field error state. Sets [data-invalid] on the root element. |
label | string | - | The label for the field. |
size | "small" | - | The size of the field. |
spread | boolean | false | Spreads the label/description and input to opposite ends. |
endText | string | - | Supporting text displayed below the field. |
variant | "filled" | - | The visual variant of the field. |
Slots
| Slot | Description |
|---|---|
label | Slot for the label element. |
description | Slot for the description (start text) element, displayed above the field. |
prefix | Content placed at the inline-start of the field, inside the border. |
suffix | Content placed at the inline-end of the field, inside the border. |
header | Content placed above the input, inside the border, with a divider. |
footer | Content placed below the input, inside the border, with a divider. |
end-text | Slot for the supporting text (end text) element. |
supporting-text | Legacy slot for the supporting text element. |
Text input API
| Prop | Type | Default | Description |
|---|---|---|---|
autoFit | boolean | false | Automatically adjusts its width to content. |
size | "small" | - | The size of the text field. |
variant | "filled" | - | The visual variant of the text field. |
Browser support
field-sizing.
Full support Supported since v26.2. See also the full browser support guide.
Installation
@layer components.extended {
/*- Common styling for input, textarea and select- Form related styles such as: label, supporting text, error handling*/ :where(.text-field, .textarea, .select) { --_accent-color: var(--primary); --_bg-color: var(--surface-default); --_border-color: var(--field-border-color); --_field-padding-block: var(--size-2); --_field-padding-inline: var(--size-2); --_filled-border-color: var(--text-primary); --_height: var(--field-size); --_label-color: var(--text-muted); --_end-text-color: var(--text-muted);
display: grid; gap: var(--size-1) 0; position: relative;
/* Field - the actual input with a border around it */ .field { background-color: var(--_bg-color); border: var(--field-border-width) solid var(--_border-color); border-radius: var(--field-border-radius); display: grid; grid-column: 1/-1; grid-row: 2; grid-template-columns: auto 1fr auto; grid-template-areas: "header header header" "prefix input suffix" "footer footer footer"; min-block-size: var(--_height);
@media (prefers-reduced-motion: no-preference) { transition: border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1); } }
:where(.field) :where(input, textarea, select) { background: transparent; border: 0; color: var(--text-primary); font-family: var(--font-sans); font-size: var(--font-size-1); grid-area: input; inline-size: 100%; line-height: var(--font-lineheight-1); min-inline-size: 0; padding: var(--_field-padding-block) var(--_field-padding-inline);
&:focus, &:focus-visible { outline: 0; } }
/* Affixes */ .prefix, .suffix { align-items: center; color: var(--_label-color); display: inline-flex; padding-inline: var(--_field-padding-inline); white-space: nowrap; }
.prefix { grid-area: prefix; }
.suffix { grid-area: suffix; }
.header, .footer { align-items: center; color: var(--_label-color); display: flex; font-size: var(--font-size-0); gap: var(--size-2); padding: var(--size-1) var(--_field-padding-inline); }
.header { border-block-end: 1px solid var(--_border-color); grid-area: header; }
.footer { border-block-start: 1px solid var(--_border-color); grid-area: footer; }
/* Remove padding on the input when affixes are next to it */ .field:has(> .prefix) :where(input, textarea, select) { padding-inline-start: 0; }
.field:has(> .suffix) :where(input, textarea, select) { padding-inline-end: 0; }
/* Textarea - keep affix aligned with the first line */ &.textarea .field {
.prefix, .suffix { align-self: start; padding-block: var(--_field-padding-block); } }
/* Required/Invalid */ &:has(:invalid) { .label:after { color: var(--red); content: "*"; margin: -0.25em auto auto 0.25em; } }
/* File */ &:has(input[type="file"]) { cursor: pointer;
input { align-self: flex-start; block-size: var(--_height); box-shadow: none; color: var(--text-primary); cursor: inherit; max-inline-size: 100%; padding: 0; transition: font-size 0.2s var(--ease-3);
&::-webkit-file-upload-button, &::file-selector-button { background-color: var(--surface-tonal); block-size: calc(100% - var(--size-2) * 2); border: none; border-radius: var(--field-border-radius); cursor: pointer; margin-block-start: var(--size-2); margin-inline-end: 1ex; margin-inline-start: var(--size-2); } }
/* Variants */ &.filled { input {
&::-webkit-file-upload-button, &::file-selector-button { background-color: var(--surface-default); block-size: calc(100% - var(--size-2) * 2); border-radius: var(--field-border-radius); cursor: pointer; margin-block-start: var(--size-2); } } }
/* Sizes */ &.small { input { font-size: var(--font-size-05);
&::-webkit-file-upload-button, &::file-selector-button { block-size: calc(100% - var(--size-2)); margin-block-start: var(--size-1); } } } }
/* Autosuggest */ &:has(input[list]) { .label { /* Make sure chevron is visible */ inline-size: calc(100% - var(--size-6)); } }
/* Select */ &:has(select) { .label { /* Make sure chevron is visible */ inline-size: calc(100% - var(--size-6));
} }
/* Customizable Select */ &:has(select button) { select { padding: 0;
button { outline: 0; padding: var(--_field-padding-block) var(--size-8) var(--_field-padding-block) var(--_field-padding-inline); } } }
/* Non-customizable Select */ &:has(select):not(:has(button)) { select { padding: var(--_field-padding-block) var(--size-8) var(--_field-padding-block) var(--_field-padding-inline); } }
/* Input - color */ &:has(input[type="color"]) { .field { block-size: var(--_height); inline-size: var(--_height); min-block-size: 0; overflow: hidden; }
input { appearance: none; background: none; block-size: var(--_height); inline-size: var(--_height); overflow: hidden; padding: 0;
&::-webkit-color-swatch { border: none; }
&::-webkit-color-swatch-wrapper { padding: 0; } } }
/* Element states */ &:hover { &:not([data-invalid]) { .field { border-color: var(--text-primary); } } }
&:focus-within { .field { border-color: var(--_accent-color); outline-offset: -2px; } }
/* Label */ .label { color: var(--text-primary); font-size: var(--font-size-05); font-weight: 600; grid-column: 1/-1; grid-row: 1; padding-inline: 0 1ex; }
/* End text */ :where(.end-text) { color: var(--_end-text-color); font-size: var(--font-size-0); grid-column: 1/-1; grid-row: 3; line-height: 1.5; }
/* Auto-fit */ &.auto-fit { inline-size: auto;
.field { inline-size: auto; }
:where(& input, & textarea) { field-sizing: content; inline-size: auto; min-inline-size: 25ch; } }
/* Validation */ &[data-invalid], &:has(:user-invalid) { --_accent-color: var(--color-9); --_border-color: var(--color-9); --_filled-border-color: var(--color-9); --_label-color: var(--color-9); --_end-text-color: var(--color-9); }
/* * Variant: Filled */ &.filled { --_bg-color: var(--surface-tonal);
&:not(:has([disabled], :has(input[type="color"]))) {
/* Hover */ &:hover { --_bg-color: light-dark(oklch(from var(--surface-tonal) calc(l * 0.93) c h), oklch(from var(--surface-tonal) calc(l * 1.1) c h)); } } }
/* Disabled */ &:where(:has([disabled])) { cursor: not-allowed; opacity: 0.64; user-select: none;
&::before { display: none; }
.field { cursor: not-allowed; }
:where(input, textarea, select) { cursor: not-allowed;
* { pointer-events: none; } }
.label, .start-text, .end-text { cursor: not-allowed; } }
/* Read-only */ &:where(:has([readonly])) { &::before { display: none; }
:where(input, textarea, select) { cursor: not-allowed;
* { pointer-events: none; } } }
/* Sizes */ &.small { --_field-padding-block: var(--size-2); --_height: var(--field-size-small);
&:has(input[type="color"]) { .label { line-height: 1.5; } } }
/* Orientation */ &.spread { align-items: start; column-gap: var(--size-4); container-type: inline-size; grid-template-columns: 1fr auto;
.label { font-weight: 600; grid-column: 1; grid-row: 1; }
.start-text { color: var(--_end-text-color); font-size: var(--font-size-0); grid-column: 1; grid-row: 2; line-height: 1.5; }
.field { align-self: start; grid-column: 2; grid-row: 1 / span 2; }
.field:has(textarea) { grid-row: 1 / span 3;
+.end-text { grid-row: 4; } }
:where(textarea) { min-inline-size: 30ch; }
:where(.end-text) { grid-column: 2; text-align: end; }
/* Collapse to column on narrow containers */ @container (width < 400px) { column-gap: 0; grid-template-columns: 1fr;
.label { grid-column: 1; grid-row: 1; }
.start-text { grid-column: 1; grid-row: 2; }
.field, .field:has(textarea) { grid-column: 1; grid-row: 3; }
:where(.end-text) { grid-column: 1; grid-row: 4; text-align: start; } } } }}@layer components.extended {
:where(.text-field:has( :where(input[type="date"], input[type="datetime-local"], input[type="email"], input[type="month"], input[type="number"], input[type="password"], input[type="search"], input[type="tel"], input[type="text"], input[type="time"], input[type="url"], input[type="week"]))) {
/* Sizes */ &.small { input { padding-inline: var(--size-2); } } }
/* Autosuggest */ :where(.text-field:has(input[list])) { .field { position: relative; }
/* Hide native arrow */ input[list]::-webkit-calendar-picker-indicator { cursor: pointer; opacity: 0; pointer-events: none; position: absolute; }
input[list] { padding-inline-end: var(--size-8); }
.field::after { block-size: 0; border-block-start: 5px solid; border-inline: 5px solid transparent; content: ""; inline-size: 0; inset: 50% var(--size-3) auto auto; pointer-events: none; position: absolute; translate: 0 -50%; }
&.small { input[list] { padding-inline-end: var(--size-7); }
.field::after { inset-inline-end: var(--size-2); } } }}