Chips

Chips are compact actions and choices in the flow of the content. Material 3 has four: assist for a helpful action, suggestion for something the app proposes, filter for narrowing a list, and input for what someone has already 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>
<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 carries a generated suggestion rather than an action, so it is text only and takes no icon. Everything else matches an assist chip.

<script lang="ts">
	import { ChipSet, SuggestionChip } from 'noph-ui'
</script>

<ChipSet>
	<SuggestionChip label="Sounds good" />
	<SuggestionChip label="On my way" />
	<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>
</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

The chip body is a button of its own, so it can be focused and activated with the keyboard. Use onclick for the primary action, for example selecting the chip or switching it into an edit mode, and onremove for the trailing icon.

<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>

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 pendingDeleteIndex: number | null = $state(null)
</script>

<TextField
	type="email"
	label="Emails"
	variant="outlined"
	placeholder="Add email..."
	style="width:340px"
	bind:value={email}
	populated={emails.length > 0}
	onkeydown={(e) => {
		if (e.key === 'Enter') {
			e.preventDefault()
			if (e.currentTarget.value && e.currentTarget.reportValidity()) {
				emails.push(e.currentTarget.value)
				email = ''
			}
		} else if (e.key === 'Backspace' && !e.currentTarget.value && emails.length > 0) {
			e.preventDefault()
			if (pendingDeleteIndex !== null) {
				emails.splice(pendingDeleteIndex, 1)
				pendingDeleteIndex = null
			} else {
				pendingDeleteIndex = emails.length - 1
			}
		} else {
			pendingDeleteIndex = null
		}
	}}
	onfocus={() => {
		pendingDeleteIndex = null
	}}
	onblur={(e) => {
		if (e.currentTarget.value && e.currentTarget.reportValidity()) {
			emails.push(e.currentTarget.value)
			email = ''
		} else {
			pendingDeleteIndex = null
		}
	}}
>
	<ChipSet>
		{#each emails as email, index (index)}
			<InputChip
				name="email"
				value={email}
				selected={pendingDeleteIndex === index}
				onremove={() => {
					if (index > -1) {
						emails.splice(index, 1)
						pendingDeleteIndex = null
					}
				}}
			/>
		{/each}
	</ChipSet>
</TextField>

Accessibility

Every chip type renders the element that matches what it does. An assist chip is a <button>, or an <a> when you pass an href. A filter chip is a checkbox with a label, so it reports its selected state to assistive technology on its own. An input chip is a button with a second button for removing it.

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, and Home and End jump to the first and the last one.

A remove button carries no text of its own, so give it one through removeAriaLabel. The default is the bare "Remove", which does not say what is being removed, so name the chip in it.

<InputChip
	label={email}
	removeAriaLabel="Remove {email}"
	onremove={() => remove(email)}
/>

Theming

TokenDefault 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

The colours of a filter and an input chip come from the theme's secondary-container and on-secondary-container roles while they are selected, and from on-surface-variant while they are not. 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

Everything you pass is forwarded to the wrapping <div>, so class, style and aria-label work as expected. bind:element gives you that <div>. The set makes room for its chips on its own, from whether it has any, so there is nothing to configure.

AssistChip attributes

An assist chip renders a <button>, or an <a> when you give it an href, and takes that element's attributes: onclick, type, class, aria-* and the rest. bind:element gives you whichever element it rendered. It does not take size or shape: a chip has one of each.

AttributeTypeDefaultDescription
labelstring''Text of the chip.
iconSnippet | undefinedundefinedLeading icon.
variant'outlined' | 'elevated''outlined'elevated drops the outline and gives the chip a shadow instead.
disabledbooleanfalseMakes the chip non-interactive.
elementHTMLDivElement | undefinedundefinedBindable reference to the chip element.

FilterChip attributes

AttributeTypeDefaultDescription
labelstring''Text of the chip.
selectedboolean | undefinedundefinedBindable. Whether the filter is on.
group(string | number)[] | null | undefinedundefinedBindable. Bind several chips that share a name to one array and their values are added and removed as they are toggled.
namestring | undefinedundefinedForm field name of the underlying checkbox.
valuestring | undefinedundefinedValue submitted with the form, and the entry used in group.
defaultSelectedboolean | null | undefinedundefinedSelected state a form reset returns the chip to.
removablebooleanfalseAdds a trailing remove button.
onremoveMouseEventHandler | undefinedundefinedCalled when the remove button is clicked.
removeAriaLabelstring'Remove'Accessible name of the remove button.
iconSnippet | undefinedundefinedLeading icon, shown while the chip is not selected.
variant'outlined' | 'elevated''outlined'elevated drops the outline and gives the chip a shadow instead.
disabledbooleanfalseMakes the chip non-interactive.
elementHTMLDivElement | undefinedundefinedBindable reference to the chip element.

InputChip attributes

AttributeTypeDefaultDescription
labelstring''Text of the chip. Falls back to value when it is empty.
selectedboolean | undefinedundefinedBindable. Highlights the chip, for instance to show which one Backspace would remove next.
namestring | undefinedundefinedForm field name. The value is submitted through a hidden input.
valuestring | number | undefinedundefinedValue submitted with the form.
onclickMouseEventHandler | undefinedundefinedCalled when the chip itself is clicked, not the remove button.
onremoveMouseEventHandler | undefinedundefinedCalled when the remove button is clicked.
removeAriaLabelstring'Remove'Accessible name of the remove button.
iconSnippet | undefinedundefinedLeading icon or avatar.
disabledbooleanfalseMakes the chip non-interactive.
elementHTMLDivElement | undefinedundefinedBindable reference to the chip element.
actionElementHTMLButtonElement | undefinedundefinedBindable reference to the chip's own button.