Chips
Chips are small actions and choices inside the content. Material 3 has four types: assist chips for a helpful action, suggestion chips for something the app suggests, filter chips to narrow a list, and input chips for something the user has entered.
Assist chip
Outlined
<script lang="ts">
import { AssistChip, ChipSet } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<AssistChip label="Assist chip" />
<AssistChip label="With icon">
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</AssistChip>
</ChipSet>
Elevated
<script lang="ts">
import { AssistChip, ChipSet } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<AssistChip variant="elevated" label="Assist chip" />
<AssistChip variant="elevated" label="With icon">
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</AssistChip>
</ChipSet>
Disabled
<script lang="ts">
import { AssistChip, ChipSet } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<AssistChip disabled label="Outlined" />
<AssistChip disabled label="Outlined with icon">
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</AssistChip>
<AssistChip disabled variant="elevated" label="Elevated" />
<AssistChip disabled variant="elevated" label="Elevated with icon">
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</AssistChip>
</ChipSet>
As link
<script lang="ts">
import { AssistChip, ChipSet } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<AssistChip label="Visit website" href="https://noph.dev" target="_blank" rel="noopener">
{#snippet icon()}<Icon>open_in_new</Icon>{/snippet}
</AssistChip>
</ChipSet>
Suggestion chip
A suggestion chip shows a generated suggestion instead of an action. It can start with an icon when that helps tell suggestions apart. Otherwise it works like an assist chip.
<script lang="ts">
import { ChipSet, SuggestionChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<SuggestionChip label="Sounds good" />
<SuggestionChip label="On my way" />
<SuggestionChip label="Coffee nearby">
{#snippet icon()}<Icon>coffee</Icon>{/snippet}
</SuggestionChip>
<SuggestionChip variant="elevated" label="Elevated" />
<SuggestionChip disabled label="Disabled" />
</ChipSet>
Filter chip
Outlined
<script lang="ts">
import { ChipSet, FilterChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<FilterChip label="Filter chip" />
<FilterChip label="Selected" selected />
<FilterChip label="With icon">
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</FilterChip>
<FilterChip label="Selected with icon" selected>
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</FilterChip>
<FilterChip label="Language">
{#snippet trailingIcon()}<Icon>arrow_drop_down</Icon>{/snippet}
</FilterChip>
</ChipSet>
Elevated
<script lang="ts">
import { ChipSet, FilterChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<FilterChip variant="elevated" label="Filter chip" />
<FilterChip variant="elevated" label="Selected" selected />
<FilterChip variant="elevated" label="With icon">
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</FilterChip>
<FilterChip variant="elevated" label="Selected with icon" selected>
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</FilterChip>
</ChipSet>
Removable
<script lang="ts">
import { ChipSet, FilterChip, Snackbar } from 'noph-ui'
let snackbar: ReturnType<typeof Snackbar> | undefined = $state()
</script>
<ChipSet>
<FilterChip label="Removable" removable onremove={() => snackbar?.show()} />
<FilterChip label="Selected" removable selected onremove={() => snackbar?.show()} />
<FilterChip variant="elevated" label="Elevated" removable onremove={() => snackbar?.show()} />
</ChipSet>
<Snackbar bind:this={snackbar} label="Remove was clicked" />
Disabled
<script lang="ts">
import { ChipSet, FilterChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<FilterChip disabled label="Outlined" />
<FilterChip disabled label="Outlined selected" selected />
<FilterChip disabled variant="elevated" label="Elevated" />
<FilterChip disabled variant="elevated" label="Elevated selected" selected />
<FilterChip disabled label="With icon">
{#snippet icon()}<Icon>bookmark</Icon>{/snippet}
</FilterChip>
</ChipSet>
Group
<script lang="ts">
import { ChipSet, FilterChip } from 'noph-ui'
let filterGroup = $state<string[]>([])
</script>
<ChipSet>
<FilterChip label="Running" value="running" bind:group={filterGroup} />
<FilterChip label="Walking" value="walking" bind:group={filterGroup} />
<FilterChip label="Cycling" value="cycling" bind:group={filterGroup} />
<FilterChip label="Swimming" value="swimming" bind:group={filterGroup} />
</ChipSet>
Input chip
Default
<script lang="ts">
import { ChipSet, InputChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<InputChip label="Input chip" />
<InputChip label="With icon">
{#snippet icon()}<Icon>person</Icon>{/snippet}
</InputChip>
</ChipSet>
Selected
<script lang="ts">
import { ChipSet, InputChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<InputChip selected label="Selected" />
<InputChip selected label="With icon">
{#snippet icon()}<Icon>person</Icon>{/snippet}
</InputChip>
</ChipSet>
Separate actions
Without onclick, an input chip can only be removed. The whole chip is one focusable
element: its remove button. Pass onclick to add a primary action, such as selecting the
chip or switching it into an edit mode. The chip body then becomes its own button next to the remove
button. The chip is then at least 88px wide, so both buttons keep a 48px target.
<script lang="ts">
import { ChipSet, InputChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
let editableLabel = $state('Fabian Reza')
let editableSelected = $state(false)
</script>
<ChipSet>
<InputChip
bind:selected={editableSelected}
label={editableLabel}
removeAriaLabel="Remove {editableLabel}"
onclick={() => (editableSelected = !editableSelected)}
onremove={() => (editableLabel = 'Removed')}
>
{#snippet icon()}<Icon>person</Icon>{/snippet}
</InputChip>
</ChipSet>
Avatar and elevated
avatar shows a 24px image, such as a person's photo, at the start of the chip instead
of an icon. variant="elevated" puts the chip on a raised surface instead of an outline.
<script lang="ts">
import { ChipSet, InputChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
const avatarOf = (name: string) =>
`data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" fill="#7d5260"/><text x="12" y="16" font-size="11" text-anchor="middle" fill="white" font-family="sans-serif">${name[0]}</text></svg>`,
)}`
</script>
<ChipSet>
<InputChip label="Hannah">
{#snippet avatar()}<img alt="" src={avatarOf('Hannah')} />{/snippet}
</InputChip>
<InputChip variant="elevated" label="Elevated">
{#snippet icon()}<Icon>person</Icon>{/snippet}
</InputChip>
</ChipSet>
Disabled
<script lang="ts">
import { ChipSet, InputChip } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
</script>
<ChipSet>
<InputChip disabled label="Disabled" />
<InputChip disabled selected label="Disabled selected" />
<InputChip disabled label="With icon">
{#snippet icon()}<Icon>person</Icon>{/snippet}
</InputChip>
</ChipSet>
Chips with text fields
<script lang="ts">
import { ChipSet, InputChip, TextField } from 'noph-ui'
let emails: string[] = $state(['info@noph.dev'])
let email = $state('')
let chipSet: HTMLDivElement | undefined = $state()
let input: HTMLInputElement | HTMLTextAreaElement | undefined = $state()
const add = (field: HTMLInputElement | HTMLTextAreaElement) => {
if (!field.value || !field.reportValidity()) return
if (!emails.includes(field.value)) emails.push(field.value)
email = ''
}
</script>
<TextField
type="email"
label="Emails"
variant="outlined"
placeholder="Add email..."
style="width:340px"
bind:value={email}
populated={emails.length > 0}
bind:inputElement={input}
onkeydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
add(e.currentTarget)
} else if (e.key === 'Backspace' && !e.currentTarget.value && emails.length > 0) {
// Backspace in the empty field moves to the last email, and Backspace there removes it.
e.preventDefault()
chipSet
?.querySelectorAll('button')
.item(emails.length - 1)
?.focus()
}
}}
onblur={(e) => add(e.currentTarget)}
>
<ChipSet bind:element={chipSet}>
{#each emails as address (address)}
<InputChip
name="email"
value={address}
onremove={() => {
emails = emails.filter((entry) => entry !== address)
// With the last email gone there is no chip left to focus, so go back to the field.
if (!emails.length) input?.focus()
}}
/>
{/each}
</ChipSet>
</TextField>
Accessibility
Each chip type renders the element that fits its role. An assist chip is a <button>, or an <a> when you pass an href. A
filter chip is a checkbox with a label, so assistive technology reads its selected state. Enter selects it like Space instead of submitting the form. An input chip is
a remove button. With onclick, it is a button plus a second button for removing it. Backspace and Delete remove the focused input chip and move focus to its neighbour.
Wrap chips in a ChipSet to give them a single tab stop. Tab moves into and
out of the set. ← and → move between the chips and wrap around at the ends. Home and End jump to the first and the last chip. When the chips wrap onto
several rows, ↑ and ↓ move to the closest chip in the row above or below.
A remove button has no visible text. Its name is "Remove" plus the chip's label, for example
"Remove Ada". Pass removeAriaLabel to change it, for example for another language.
Theming
| Token | Default value |
|---|---|
--np-assist-chip-label-text-color | --np-color-on-surface |
--np-assist-chip-outline-color | --np-color-outline-variant |
--np-filter-chip-container-shape | --np-shape-corner-small |
--np-filter-chip-outline-color | --np-color-outline-variant |
--np-input-chip-container-shape | --np-shape-corner-small |
--np-input-chip-outline-color | --np-color-outline-variant |
Selected filter and input chips take their colours from the theme's secondary-container and on-secondary-container roles. Unselected ones
use on-surface-variant. An elevated chip takes its shadow from the elevation roles.
Example
<script lang="ts">
import { AssistChip, ChipSet, FilterChip } from 'noph-ui'
</script>
<ChipSet>
<FilterChip
label="Tertiary"
--np-filter-chip-container-shape="var(--np-shape-corner-full)"
--np-filter-chip-outline-color="var(--np-color-tertiary)"
/>
<AssistChip
label="Tertiary"
--np-assist-chip-outline-color="var(--np-color-tertiary)"
--np-assist-chip-label-text-color="var(--np-color-tertiary)"
/>
</ChipSet>
API
ChipSet attributes
All attributes go to the wrapping <div>, including class, style and aria-label. bind:element gives you that <div>.
AssistChip attributes
An assist chip renders a <button>, or an <a> when you give
it an href. It takes that element's attributes, such as onclick, type, class and aria-*. bind:element gives you the
rendered element.
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | '' | Text of the chip. |
icon | Snippet | undefined | undefined | Leading icon. |
variant | 'outlined' | 'elevated' | 'outlined' | elevated replaces the outline with a shadow. |
disabled | boolean | false | Makes the chip non-interactive. |
element | HTMLDivElement | undefined | undefined | Bindable reference to the chip element. |
FilterChip attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | '' | Text of the chip. |
selected | boolean | undefined | undefined | Bindable. Whether the filter is on. |
group | (string | number)[] | null | undefined | undefined | Bindable. Bind chips that share a name to one array. Each chip adds or removes
its value when toggled. |
name | string | undefined | undefined | Form field name of the underlying checkbox. |
value | string | undefined | undefined | Value submitted with the form, and the entry used in group. |
defaultSelected | boolean | null | undefined | undefined | Selected state a form reset returns the chip to. |
removable | boolean | false | Adds a trailing remove button. |
onremove | MouseEventHandler | undefined | undefined | Called when the remove button is clicked. |
removeAriaLabel | string | 'Remove ' + label | Accessible name of the remove button. |
trailingIcon | Snippet | undefined | undefined | Trailing icon, such as an arrow for a chip that opens a menu. Not shown when removable. |
icon | Snippet | undefined | undefined | Leading icon, shown while the chip is not selected. |
variant | 'outlined' | 'elevated' | 'outlined' | elevated replaces the outline with a shadow. |
disabled | boolean | false | Makes the chip non-interactive. |
element | HTMLDivElement | undefined | undefined | Bindable reference to the chip element. |
InputChip attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | '' | Text of the chip. Falls back to value when it is empty. |
selected | boolean | undefined | undefined | Bindable. Highlights the chip, for example to show which one Backspace would remove next. |
name | string | undefined | undefined | Form field name. The value is submitted through a hidden input. |
value | string | number | undefined | undefined | Value submitted with the form. |
onclick | MouseEventHandler | undefined | undefined | Called when the chip itself is clicked, not the remove button. |
onremove | MouseEventHandler | undefined | undefined | Called when the remove button is clicked. |
removeAriaLabel | string | 'Remove ' + label | Accessible name of the remove button. |
icon | Snippet | undefined | undefined | Leading icon. |
avatar | Snippet | undefined | undefined | A 24px leading image, such as a photo, in place of icon. |
variant | 'outlined' | 'elevated' | 'outlined' | Outlined, or on a raised surface. |
disabled | boolean | false | Makes the chip non-interactive. |
element | HTMLDivElement | undefined | undefined | Bindable reference to the chip element. |
actionElement | HTMLButtonElement | undefined | undefined | Bindable reference to the chip's own button. |