Draw the text field's chrome without Tailwind

<x-field> renders data-md-field with its props and parts as data-md-*
attributes (box, control, label, outline, support, counter, trailing
buttons), and field.css moves into material.components on tokens: px
geometry, spacing and state tokens, the 600px breakpoint (plan step 36).
Its icons take a size prop, 24/20/16 by the field's size.

Every control the field wraps now marks itself data-md-field-control,
so the inputs, the pickers, choices, field.js, menu.css's select and
listbox rules and timepicker.css follow the renamed hooks. Browser tests
cover the error icon, the disabled field's hover, the counter and the
width bound from 600px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 14:08:22 +02:00
co-authored by Claude Opus 5
parent a6e5a593d5
commit 0341f9d4ca
24 changed files with 478 additions and 322 deletions
+23 -23
View File
@@ -6,7 +6,7 @@
* - `<x-select>`'s: the native <select>, opted into the browser's customizable select
* (`appearance: base-select`). `::picker(select)` is the menu and each <option> a row, while the
* keyboard, type-to-find, the screen reader and the form value stay the browser's own.
* - `<x-choices searchable>`'s listbox, which Alpine draws, as `.field-menu` and `.field-option`.
* - `<x-choices searchable>`'s listbox, which Alpine draws, as `[data-md-field-menu]` and `[data-md-field-option]`.
*
* M3's menu as its tokens have it: surface-container at elevation 2, extra-small corner, 48px rows in
* body-large, the chosen row in secondary-container with a tick at its end (so it is not told by colour
@@ -149,7 +149,7 @@
}
}
.field-menu {
[data-md-field-menu] {
max-block-size: 18rem;
overflow-y: auto;
padding-block: 0.5rem;
@@ -160,7 +160,7 @@
/* As a popover (`<x-choices searchable>`), it hangs under its field, as wide, and goes above only
when there is no room below. */
.field-menu[popover] {
[data-md-field-menu][popover] {
inset: auto;
top: anchor(bottom);
left: anchor(left);
@@ -172,12 +172,12 @@
position-try-fallbacks: flip-block;
}
.field-option[aria-disabled="true"] {
[data-md-field-option][aria-disabled="true"] {
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
cursor: default;
}
.field-option {
[data-md-field-option] {
display: flex;
align-items: center;
gap: 0.75rem;
@@ -190,30 +190,30 @@
}
/* `data-active` is the row the arrow keys or the pointer are on. */
.field-option[data-active] {
[data-md-field-option][data-active] {
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
}
.field-option[aria-selected="true"] {
[data-md-field-option][aria-selected="true"] {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
.field-option[aria-selected="true"][data-active] {
[data-md-field-option][aria-selected="true"][data-active] {
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
}
.field-check {
[data-md-field-check] {
margin-inline-start: auto;
}
.field-option:not([aria-selected="true"]) .field-check {
[data-md-field-option]:not([aria-selected="true"]) [data-md-field-check] {
visibility: hidden;
}
@supports (appearance: base-select) {
select.field-control,
select.field-control::picker(select) {
select[data-md-field-control],
select[data-md-field-control]::picker(select) {
appearance: base-select;
}
@@ -223,7 +223,7 @@
order tries the roomier side first, which opened a menu upwards with room to
spare under it. It opens with a fade and a grow on the spatial spring and
closes with a fade, as `<x-menu>` does. */
select.field-control::picker(select) {
select[data-md-field-control]::picker(select) {
position-try-order: normal;
max-block-size: 18rem;
max-inline-size: calc(100vw - 2rem);
@@ -242,7 +242,7 @@
transition-behavior: allow-discrete;
}
select.field-control:open::picker(select) {
select[data-md-field-control]:open::picker(select) {
opacity: 1;
transition-property: opacity, scale, display, overlay;
transition-duration: var(--md-sys-motion-spatial-fast-duration);
@@ -250,13 +250,13 @@
}
@starting-style {
select.field-control:open::picker(select) {
select[data-md-field-control]:open::picker(select) {
opacity: 0;
scale: 0.95;
}
}
select.field-control option {
select[data-md-field-control] option {
display: flex;
align-items: center;
gap: 0.75rem;
@@ -272,31 +272,31 @@
}
@media (hover: hover) {
select.field-control option:hover {
select[data-md-field-control] option:hover {
background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
}
}
select.field-control option:focus-visible {
select[data-md-field-control] option:focus-visible {
background: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
}
select.field-control option:checked {
select[data-md-field-control] option:checked {
background: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-on-secondary-container);
}
@media (hover: hover) {
select.field-control option:checked:hover {
select[data-md-field-control] option:checked:hover {
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 8%, var(--md-sys-color-secondary-container));
}
}
select.field-control option:checked:focus-visible {
select[data-md-field-control] option:checked:focus-visible {
background: color-mix(in srgb, var(--md-sys-color-on-secondary-container) 10%, var(--md-sys-color-secondary-container));
}
select.field-control option:disabled {
select[data-md-field-control] option:disabled {
background: transparent;
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
cursor: default;
@@ -305,7 +305,7 @@
/* The tick, at the row's end: the browser draws it before the label, and hides
it on every row but the chosen one. The Material Symbol, as a mask, so it
takes the row's ink. */
select.field-control option::checkmark {
select[data-md-field-control] option::checkmark {
content: "";
order: 1;
flex: none;