{{-- An M3 chip — assist, filter, input or suggestion — in one component, picked by `type`.
`assist` (the default) is an action: a button, or an anchor with `link` (`wire:navigate` unless
`external` or `no-wire-navigate`; `disabled` works on a link too, as `aria-disabled`). Its icons
are primary. `suggestion` is the same in on-surface-variant, for a suggested reply or query.
`filter` is a toggle. With `wire:model`, `x-model` or `name` it is a native checkbox under the
chip, so Livewire and Alpine bind it as they bind any checkbox — to a boolean, or to an array
with `value` for a multi-select set — and a form submits it; `selected` is then only the
initial state without a model. Without any of them it is a toggle button whose `selected`
(`aria-pressed`) the caller owns, flipped by its own `wire:click`. Selected, it takes
secondary-container and a check that grows in at the start; with an `icon`, the check takes the
icon's place.
`input` is something a person entered: `avatar` (an image URL, or initials) or `icon` at the
start, `selected`, and `removable` for a trailing remove button named "Remove ". Removing
calls `wire:remove` (a Livewire action) or `remove` (an Alpine expression); with neither, the
chip takes itself off the page, hidden `name`/`value` input and all. Backspace or Delete on a
focused chip removes it too, and focus moves to the previous (Backspace) or next (Delete) chip.
The chip is a button only when it has something of its own to do (`wire:click`, `x-on:click`,
`link`, `selected`); otherwise the remove button is its one stop. Give a removable chip in a
Livewire loop a `wire:key`.
`elevated` draws assist, filter and suggestion chips on surface-container-low at elevation 1
instead of the outline; input chips are flat only, as in M3. `tooltip` attaches a plain tooltip.
Attributes: on a filter checkbox and an input chip, `class`, `style` and `wire:key` stay on the
chip and every other attribute goes to the control inside (the checkbox, or the chip's button).
Values from androidx Compose Material 3 (Chip.kt, AssistChipTokens, FilterChipTokens,
InputChipTokens, SuggestionChipTokens at androidx/androidx 27cf9a7, Apache-2.0): 32px tall,
small corner, label-large, 18px icons, 24px avatar; 16px padding beside a label, 8px beside an
icon, 8px between (an input chip: 12px, 8px, 4px beside an avatar). The check grows in on the
fast spatial spring and fades in on the slow effects one; it shrinks on default effects and
fades on fast effects, as AnimatingChipContent does. --}}
@props([
'type' => 'assist',
'label' => null,
'icon' => null,
'iconRight' => null,
'avatar' => null,
'elevated' => false,
'selected' => null,
'removable' => false,
'remove' => null,
'link' => null,
'external' => false,
'noWireNavigate' => false,
'disabled' => false,
'name' => null,
'value' => null,
'tooltip' => null,
])
@php
$kind = in_array($type, ['assist', 'filter', 'input', 'suggestion'], true) ? $type : 'assist';
$text = $label ?? trim(strip_tags((string) $slot));
$isLink = filled($link) && $kind !== 'filter';
$inert = $disabled && $isLink;
$raised = $elevated && $kind !== 'input';
$initials = filled($avatar) && ! str_contains((string) $avatar, '/') && ! str_contains((string) $avatar, '.');
$anchor = $tooltip !== null ? '--material-chip-'.\Illuminate\Support\Str::lower(\Illuminate\Support\Str::random(10)) : null;
// A filter chip is a native checkbox whenever something binds or submits it.
$checkbox = $kind === 'filter' && ($attributes->whereStartsWith(['wire:model', 'x-model'])->isNotEmpty() || filled($name));
$wireModel = $attributes->whereStartsWith('wire:model')->first();
$checked = (bool) $selected;
// Rendered as the bound property already says, so nothing flips (and animates) once Livewire starts.
if ($checkbox && $wireModel !== null && ($component = \Livewire\Livewire::current()) !== null) {
$bound = data_get($component, $wireModel);
$checked = is_array($bound)
? in_array((string) $value, array_map(fn ($item): string => is_scalar($item) ? (string) $item : '', $bound), true)
: (bool) $bound;
}
$wireRemove = $attributes->get('wire:remove');
$attributes = $attributes->except(['wire:remove']);
$outer = ['class', 'style', 'wire:key'];
// Compose draws the 1px border inside the chip; a CSS border takes room, so the padding
// beside it is 1px short of the token (7px for 8, 15px for 16).
$base = 'group/chip inline-flex h-8 max-w-full shrink-0 select-none items-center whitespace-nowrap rounded-corner-sm border type-label-lg transition-[background-color,border-color,color,box-shadow] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast';
// Below 48px the touch target reaches past the chip, as M3 requires.
$target = 'after:absolute after:inset-x-0 after:top-1/2 after:h-12 after:-translate-y-1/2';
// Every colour class written out whole, so Tailwind compiles it. Selected is a state the browser
// owns on a filter chip (a checked checkbox, or aria-pressed), so it is written as a variant.
$colours = match (true) {
$kind === 'filter' && $raised && $disabled => 'border-transparent bg-on-surface/12 text-on-surface/38',
$kind === 'filter' && $raised => 'border-transparent bg-surface-container-low text-on-surface-variant shadow-elevation-1 hover:shadow-elevation-2 has-checked:bg-secondary-container has-checked:text-on-secondary-container aria-pressed:bg-secondary-container aria-pressed:text-on-secondary-container',
$kind === 'filter' && $disabled => 'border-on-surface/12 text-on-surface/38 has-checked:border-transparent has-checked:bg-on-surface/12 aria-pressed:border-transparent aria-pressed:bg-on-surface/12',
$kind === 'filter' => 'border-outline-variant text-on-surface-variant focus-visible:border-on-surface-variant has-focus-visible:border-on-surface-variant has-checked:border-transparent has-checked:bg-secondary-container has-checked:text-on-secondary-container hover:has-checked:shadow-elevation-1 aria-pressed:border-transparent aria-pressed:bg-secondary-container aria-pressed:text-on-secondary-container hover:aria-pressed:shadow-elevation-1',
$kind === 'input' && $disabled => $selected ? 'border-transparent bg-on-surface/12 text-on-surface/38' : 'border-on-surface/12 text-on-surface/38',
$kind === 'input' => $selected ? 'border-transparent bg-secondary-container text-on-secondary-container' : 'border-outline-variant text-on-surface-variant has-[[data-chip-action]:focus-visible]:border-on-surface-variant',
$raised && $disabled => 'border-transparent bg-on-surface/12 text-on-surface/38',
$raised => $kind === 'suggestion'
? 'border-transparent bg-surface-container-low text-on-surface-variant shadow-elevation-1 hover:shadow-elevation-2'
: 'border-transparent bg-surface-container-low text-on-surface shadow-elevation-1 hover:shadow-elevation-2',
$disabled => 'border-on-surface/12 text-on-surface/38',
default => $kind === 'suggestion'
? 'border-outline-variant text-on-surface-variant focus-visible:border-on-surface-variant'
: 'border-outline-variant text-on-surface focus-visible:border-on-surface',
};
// Leading icons: primary on assist, suggestion and filter chips; on-surface-variant on an input
// chip, primary while it is hovered, focused or pressed, and primary on a selected one.
$clicks = $attributes->whereStartsWith(['wire:click', 'x-on:click', '@click'])->isNotEmpty();
$leadingInk = match (true) {
$disabled => null,
$kind !== 'input', (bool) $selected => 'text-primary',
$isLink || $clicks => 'transition-colors duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-hover/chip:text-primary group-active/chip:text-primary group-has-focus-visible/chip:text-primary',
default => null,
};
$trailingInk = in_array($kind, ['assist', 'suggestion'], true) && ! $disabled ? 'text-primary' : null;
$check = 'size-4.5 opacity-0 transition-opacity duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast group-has-checked/chip:opacity-100 group-has-checked/chip:duration-(--md-sys-motion-effects-slow-duration) group-has-checked/chip:ease-effects-slow group-aria-pressed/chip:opacity-100 group-aria-pressed/chip:duration-(--md-sys-motion-effects-slow-duration) group-aria-pressed/chip:ease-effects-slow';
if ($kind === 'input') {
$actionTag = match (true) {
$isLink => 'a',
$clicks || $selected !== null => 'button',
default => 'span',
};
$interactive = $actionTag !== 'span';
$removeExpression = implode('; ', array_filter([
'removeChip($event)',
filled($remove) ? $remove : null,
blank($remove) && blank($wireRemove) ? '$root.remove()' : null,
]));
$container = $attributes->only($outer)
->class([
$base,
'relative isolate',
$colours,
'has-[[data-chip-action]:focus-visible]:outline-3 has-[[data-chip-action]:focus-visible]:outline-offset-2 has-[[data-chip-action]:focus-visible]:outline-secondary',
])
->merge(array_filter(['style' => $anchor ? "anchor-name: {$anchor}" : null]));
$action = $attributes->except($outer)
->class([
'flex h-full min-w-0 items-center outline-none',
match (true) {
filled($avatar) => 'ps-0.75',
filled($icon) => 'ps-1.75',
default => 'ps-2.75',
},
$removable ? 'pe-2' : 'pe-2.75',
'cursor-pointer before:absolute before:-inset-px before:-z-10 before:rounded-corner-sm before:bg-current before:opacity-0 before:transition-opacity before:duration-(--md-sys-motion-effects-fast-duration) before:ease-effects-fast hover:before:opacity-8 focus-visible:before:opacity-10 active:before:opacity-10' => $interactive && ! $disabled,
$target => $interactive,
'cursor-not-allowed' => $interactive && $disabled,
'pointer-events-none' => $inert,
])
->merge(array_filter([
'data-chip-action' => $interactive ? true : null,
'href' => $isLink ? $link : null,
'target' => $isLink && $external ? '_blank' : null,
'rel' => $isLink && $external ? 'noopener' : null,
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
'aria-disabled' => $inert ? 'true' : null,
'tabindex' => $inert ? '-1' : null,
'type' => $actionTag === 'button' ? 'button' : null,
'disabled' => $actionTag === 'button' && $disabled ? true : null,
'aria-pressed' => $actionTag === 'button' && $selected !== null ? ($selected ? 'true' : 'false') : null,
], fn ($attribute): bool => $attribute !== null));
} else {
$tag = match (true) {
$checkbox => 'label',
$isLink => 'a',
default => 'button',
};
$control = $checkbox
? $attributes->except($outer)->merge(array_filter([
'type' => 'checkbox',
'name' => $name,
'value' => $value,
'checked' => $checked ? true : null,
'disabled' => $disabled ? true : null,
], fn ($attribute): bool => $attribute !== null))->class('peer sr-only')
: null;
$container = ($checkbox ? $attributes->only($outer) : $attributes)
->class([
$base,
'state-layer relative',
'focus-ring' => ! $checkbox,
'has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-secondary has-focus-visible:before:opacity-10' => $checkbox,
$kind === 'filter' || filled($icon) ? 'ps-1.75' : 'ps-3.75',
$iconRight ? 'pe-1.75' : 'pe-3.75',
$colours,
$target,
'cursor-pointer' => ! $disabled,
'cursor-not-allowed' => $disabled && ! $isLink,
'pointer-events-none' => $inert,
])
->merge(array_filter([
'data-chip' => $kind,
'href' => $isLink ? $link : null,
'target' => $isLink && $external ? '_blank' : null,
'rel' => $isLink && $external ? 'noopener' : null,
'wire:navigate' => $isLink && ! $external && ! $noWireNavigate && ! $attributes->has('wire:navigate') ? true : null,
'aria-disabled' => $inert ? 'true' : null,
'tabindex' => $inert ? '-1' : null,
'type' => $tag === 'button' ? 'button' : null,
'disabled' => $tag === 'button' && $disabled ? true : null,
'name' => $tag === 'button' ? $name : null,
'value' => $tag === 'button' ? $value : null,
'aria-pressed' => $kind === 'filter' && ! $checkbox ? ($selected ? 'true' : 'false') : null,
'style' => $anchor ? "anchor-name: {$anchor}" : null,
], fn ($attribute): bool => $attribute !== null));
}
@endphp
@if ($kind === 'input')
<{{ $actionTag }} {{ $action }}>
@if ($avatar)
@if ($initials)
$disabled])>{{ $avatar }}
@else
$disabled]) />
@endif
@elseif ($icon)
@endif
{{ $label ?? $slot }}
{{ $actionTag }}>
@if ($removable)
! $disabled,
'before:absolute before:-inset-0.75 before:rounded-corner-full before:bg-current before:opacity-0 before:transition-opacity before:duration-(--md-sys-motion-effects-fast-duration) before:ease-effects-fast hover:before:opacity-8 focus-visible:before:opacity-10 active:before:opacity-10' => ! $disabled,
'after:absolute after:-inset-x-2 after:-inset-y-3.75',
'cursor-not-allowed' => $disabled,
])
>
@endif
@if (filled($name))
@endif
@if ($tooltip !== null)
@endif
@else
<{{ $tag }} {{ $container }}>
@if ($checkbox)
@endif
@if ($kind === 'filter')
{{-- The check grows in from nothing; beside an icon it takes the icon's place. --}}
filled($icon),
'h-4.5 w-0 transition-[width] duration-(--md-sys-motion-effects-default-duration) ease-effects-default group-has-checked/chip:w-4.5 group-has-checked/chip:duration-(--md-sys-motion-spatial-fast-duration) group-has-checked/chip:ease-spatial-fast group-aria-pressed/chip:w-4.5 group-aria-pressed/chip:duration-(--md-sys-motion-spatial-fast-duration) group-aria-pressed/chip:ease-spatial-fast' => blank($icon),
])>
@if ($icon)
@endif
@elseif ($icon)
@endif
{{ $label ?? $slot }}
@if ($iconRight)
@endif
@if ($tooltip !== null)
@endif
{{ $tag }}>
@endif