GrChip
An interactive tag: removable, selectable, with an icon. Where GrBadge only shows a status, a chip answers to a click.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- a dismissible label on an entity — the tags of a task, the recipients of a letter, the selected files: a cross on each, the list kept by the application;
- a filter that is switched on and off —
selectablegives a toggle witharia-pressed, and a set of such filters is collected byGrChipGroup; - a quick choice among short options — a period, a status, a priority: chips take up a line rather than a dropdown list;
- a row of tags of your own beside a field — when the input and the storage are already written and all that is needed is a tile with a cross.
When to take something else
| Need | Take |
|---|---|
| A status or a tag without interaction | GrBadge |
| A field that collects tags from typing itself | GrInputTag |
| A counter on top of a button or an icon | GrBadgeWrap |
| A single choice among options in one line | GrSegmented |
| Many options that are searched by typing | GrSelect / GrAutocomplete |
The cross is not always a button
The markup of a chip depends on whether the chip itself is clicked.
The chip is not a widget (just a dismissible label) — the root is a <span>,
and the cross is a real <button> with a name of its own and a Tab stop of its
own. That is how the chips inside GrInputTag are built.
The chip is a widget (selectable or inside a group) — the root is a
<button>, and the cross inside it cannot be a button: the role declares its
descendants presentational (axe catches that as nested-interactive), and a
<button> inside a <button> is invalid by the HTML content model. The cross
therefore becomes an aria-hidden <span>, dismissal moves to
Delete/Backspace, and the chip announces that through aria-keyshortcuts. The
closable tabs of GrTabs live by the same device.
The practical consequence: a link must not be put inside a selectable chip. If a link is needed, the chip is not selectable.
The chip does not perform the dismissal
remove is a request, not a fact: the contents of the set belong to the consumer,
and only the consumer can remove an item from the array. The chip does not hide
itself and announces nothing into a live region — announcing “removed” before it
has happened would be a lie.
The label is needed separately from the slot
The label prop duplicates the content of the slot not by an oversight: the name of
the dismiss button is built from it. Twenty buttons named “Remove” in a row make it
impossible to pick the right one — the name has to name the tag itself, so by
default it is “Remove {label}”. If the content is more complex than a string, set
removeLabel by hand.
The selection is seen by the fill, not by the outline
A selected chip takes the solid variant of its tone — the same one dark gives.
The hue does not change in the process: a set of filters where the selected one is
recoloured into a neighbouring colour reads as a rainbow rather than as a state.
An outline and a slight thickening of the type do not carry that job: in a row of five filters the selected one is not found at a glance by them. A difference in lightness is found — and it survives monochrome and colour blindness, unlike “became slightly darker”.
The non-colour channel is kept nonetheless: a selected chip has
font-weight: 600. It is that channel that carries the case where dark is set for
the whole set and the fill of the selected and the unselected coincide.
Limits
- there is no virtualisation. A thousand chips in a row is not a row of chips
but a list: cut the set yourself or take
GrSelectwithvirtual; - chips are not reordered by dragging. The order is set by the consumer’s array;
if a manual order is needed —
GrSortableList; - a chip has no input of its own. It shows a ready value;
collecting a set from typing is
GrInputTag.
Playground 11
Loading…
<GrChip />Install
npm i @feugene/granularityImport
import { GrChip } from '@feugene/granularity/components/GrChip'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
tone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | undefined | — |
closable | boolean | undefined | false | A cross. Inside a widget it is not a button — see the section on roles below. |
disabled | boolean | undefined | false | — |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | An accessible name for when the label does not explain the purpose. |
dark | boolean | undefined | undefined | — |
radius | GrBadgeRadius | undefined | undefined | — |
value | GrChipValue | undefined | undefined | The value of the chip in a group. Outside `GrChipGroup` it is not used. |
label | string | undefined | undefined | The label. It is needed separately from the slot: the name of the dismiss button is built from it — "Remove" on twenty buttons in a row makes it impossible to pick the right one. |
selectable | boolean | undefined | false | The chip becomes a toggle: `aria-pressed`, `Enter`/`Space`. |
selected | boolean | undefined | false | The state of the toggle outside a group. Inside a group the value is run by the group. |
removeLabel | string | undefined | undefined | The name of the dismiss button as a whole. It overrides the one built from `label`. |
removeTabindex | number | undefined | undefined | The `tabindex` of the dismiss button — for a chip inside an input field. There the tab stop belongs to the `<input>`, and the row of chips is walked with the arrows: in `GrInputTag` the ring leaves exactly one `0`, in `GrAutocomplete` there are none at all. The prop is needed because the ring is run by the parent — it knows both the order of the chips and what lies beyond the edge of the row. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The label of the chip. Inside a selectable chip — phrasing content only. |
icon | any | An icon before the label. |
Events
| Event | Type | Description |
|---|---|---|
remove | [] | — |
update:selected | [value: boolean] | — |
Methods / Expose
| Methods / Expose | Type | Description |
|---|---|---|
focus | () => void | undefined | — |
blur | () => void | undefined | — |
removeEl | HTMLButtonElement | null | The dismiss button — the target of the parent’s roving focus. It is given away as an element rather than as a `focusRemove` method: the ring needs the node itself, and it decides for itself when and where to move the focus. |
Examples 3
Tags
<script setup lang="ts">
import { ref } from 'vue'
import { GrCard, GrChip } from '@feugene/granularity'
// Метки задачи: состав ведёт приложение, чип только просит его убрать.
const tags = ref(['design', 'frontend', 'нужен ревью', 'v0.25'])
function drop(tag: string): void {
tags.value = tags.value.filter(item => item !== tag)
}
</script>
<template>
<GrCard class="p-4">
<div class="flex flex-wrap items-center gap-2">
<GrChip
v-for="tag in tags"
:key="tag"
:label="tag"
tone="slate"
closable
@remove="drop(tag)"
/>
<span v-if="!tags.length" class="text-sm text-[var(--gr-muted-fg)]">
Меток не осталось
</span>
</div>
</GrCard>
</template>Icon
<script setup lang="ts">
import { ref } from 'vue'
import IconCheck from '~icons/lucide/check'
import IconUser from '~icons/lucide/user'
import { GrChip } from '@feugene/granularity'
const subscribed = ref(false)
</script>
<template>
<div class="flex flex-wrap items-center gap-2">
<GrChip label="Анна Петрова" tone="azure" closable>
<template #icon>
<IconUser class="h-full w-full" />
</template>
</GrChip>
<GrChip
v-model:selected="subscribed"
label="Подписаться на изменения"
tone="success"
selectable
>
<template v-if="subscribed" #icon>
<IconCheck class="h-full w-full" />
</template>
</GrChip>
<GrChip label="Архивная метка" tone="neutral" closable disabled />
</div>
</template>Tones
<script setup lang="ts">
import { GrChip } from '@feugene/granularity'
const tones = ['neutral', 'primary', 'success', 'warning', 'danger', 'info', 'slate', 'azure'] as const
const sizes = ['xs', 'sm', 'md', 'lg'] as const
</script>
<template>
<div class="grid gap-4">
<div class="flex flex-wrap items-center gap-2">
<GrChip v-for="tone in tones" :key="tone" :tone="tone" :label="tone" />
</div>
<div class="flex flex-wrap items-center gap-2">
<GrChip v-for="tone in tones" :key="tone" :tone="tone" :label="tone" dark />
</div>
<div class="flex flex-wrap items-center gap-2">
<GrChip v-for="size in sizes" :key="size" :size="size" tone="info" :label="size" closable />
</div>
</div>
</template>Accessibility
- APG pattern
toggle button / option