Draw the chips without Tailwind

<x-chip> renders data-md-chip with its type and its states and parts as
data-md-* attributes (elevated, disabled, selected, actionable, the
icon, check slot, avatar, action and remove button), and every colour,
size, target and transition moves into components/chip.css on tokens
(plan step 36). Icons take size 18; the filter checkbox is
md-visually-hidden. chips.js follows the renamed chip hooks.

A disabled filter chip drawn as a label no longer shows a hover state
layer, which M3 never gives a disabled control. Browser tests cover the
32px chip and the remove button's 48px target.

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:50:29 +02:00
co-authored by Claude Opus 5
parent b42069503d
commit d8072d878a
6 changed files with 606 additions and 176 deletions
+11 -11
View File
@@ -25,23 +25,23 @@ document.addEventListener('alpine:init', () => {
window.Alpine.data('materialChip', () => ({
init() {
const button = this.removeButton()
const label = this.$root.querySelector('[data-chip-label]')
const label = this.$root.querySelector('[data-md-chip-label]')
// A chip rendered by x-for has no label on the server: its button carries the translated
// words with a `:label` placeholder, filled in once Alpine has written the label.
if (button?.dataset.chipRemove && label) {
if (button?.dataset.mdChipRemove && label) {
this.$nextTick(() => {
const text = label.textContent.trim()
if (text !== '' && !button.hasAttribute('aria-label')) {
button.setAttribute('aria-label', button.dataset.chipRemove.replace(':label', text))
button.setAttribute('aria-label', button.dataset.mdChipRemove.replace(':label', text))
}
})
}
},
removeButton() {
return this.$root.querySelector('[data-chip-remove]')
return this.$root.querySelector('[data-md-chip-remove]')
},
removeOnKey(event) {
@@ -69,14 +69,14 @@ document.addEventListener('alpine:init', () => {
handOffFocus(backwards) {
const chip = this.$root
const set = chip.closest('[data-chip-set]') ?? chip.parentElement
const chips = [...(set?.querySelectorAll('[data-chip]') ?? [])]
const chips = [...(set?.querySelectorAll('[data-md-chip]') ?? [])]
const index = chips.indexOf(chip)
const before = chips.slice(0, index).reverse()
const after = chips.slice(index + 1)
const onRemove = document.activeElement?.hasAttribute('data-chip-remove')
const onRemove = document.activeElement?.hasAttribute('data-md-chip-remove')
for (const other of backwards ? [...before, ...after] : [...after, ...before]) {
const target = (onRemove && other.querySelector('[data-chip-remove]:not(:disabled)')) || (other.matches(CONTROLS) ? other : other.querySelector(CONTROLS))
const target = (onRemove && other.querySelector('[data-md-chip-remove]:not(:disabled)')) || (other.matches(CONTROLS) ? other : other.querySelector(CONTROLS))
if (target) {
target.focus()
@@ -92,17 +92,17 @@ document.addEventListener('alpine:init', () => {
// seconds pass), whenever the set changes and focus has fallen away, focus the same
// control again — the element itself, or the chip with its `wire:key` after a morph.
holdFocus(target, set) {
const key = target.closest('[data-chip]')?.getAttribute('wire:key')
const onRemove = target.hasAttribute('data-chip-remove')
const key = target.closest('[data-md-chip]')?.getAttribute('wire:key')
const onRemove = target.hasAttribute('data-md-chip-remove')
const current = () => {
if (target.isConnected || !key) {
return target.isConnected ? target : null
}
const chip = [...set.querySelectorAll('[data-chip]')].find((other) => other.getAttribute('wire:key') === key)
const chip = [...set.querySelectorAll('[data-md-chip]')].find((other) => other.getAttribute('wire:key') === key)
return chip && (onRemove ? chip.querySelector('[data-chip-remove]') : chip.matches(CONTROLS) ? chip : chip.querySelector(CONTROLS))
return chip && (onRemove ? chip.querySelector('[data-md-chip-remove]') : chip.matches(CONTROLS) ? chip : chip.querySelector(CONTROLS))
}
const observer = new MutationObserver(() => {