GrChip

  • core
  • data
  • API · 13 props
  • ⌨ keys

An interactive tag: removable, selectable, with an icon. Where GrBadge only shows a status, a chip answers to a click.

import { GrChip } from '@feugene/granularity/components/GrChip'

Overview

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 — selectable gives a toggle with aria-pressed, and a set of such filters is collected by GrChipGroup;
  • 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

NeedTake
A status or a tag without interactionGrBadge
A field that collects tags from typing itselfGrInputTag
A counter on top of a button or an iconGrBadgeWrap
A single choice among options in one lineGrSegmented
Many options that are searched by typingGrSelect / GrAutocomplete

Variants every tone and view, rendered by the real component

Variants · GrChip
tone \ darkfalsetrue
primary
neutral
success
warning
danger
info
slate
azure

Sizes

  • xs
  • sm
  • md
  • lg

States

  • normal
  • disabled

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 5 each with a live demo and its code

  • Labels you take off in one move

    closable adds a real button named after the label — «Remove vat», not «Remove» five times in a row. remove is a request, not a fact: the chip does not hide itself, the list lives in the screen. A badge reports a state; a chip is a thing the object was labelled with, so it can be taken off.

    Export fails for invoices with reverse-charge VAT#1824 · opened by Grace Wu 2 days ago
    Labels
    buginvoicesvatcustomer-reported
  • Recipients, and who is outside

    The #icon slot tells a colleague from an outside address, and the tone backs it up. When the label alone does not say what the cross removes, remove-label gives the whole name: «Remove Grace Wu from recipients».

    Send invoice INV-0418
    ToAda LinGrace WuNorthwind AP

    1 recipient is outside Acme — the invoice leaves the workspace.

  • Filters that are switched on and off

    selectable turns the chip into a toggle with aria-pressed, and v-model:selected holds its state. The selected chip takes the solid fill of its tone — found at a glance, and kept apart by weight in monochrome too. An icon-only chip is named by aria-label; disabled keeps an unavailable filter in sight.

    3 of 24 invoices · €4,035 due

  • Query tokens in a search bar

    A token is closer to code than to a tag: radius="semi" gives it the shape of a key label, size="sm" fits it into the field. The slot holds a key and a value in two weights, so label would not say what is removed — remove-label does.

    status: overduecustomer: Northwindamount: > €1,000
    3 filters · 9 invoices

API generated from the package · cannot drift

Props

PropTypedefaultDescription
disabledboolean | undefinedfalseThe chip is unavailable: it cannot be selected or dismissed, the cross is hidden. Inside a group — also from its `disabled`.
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedThe tone of the chip — the same colours as `GrBadge`, so that they do not diverge in one row. Not set — from the group and `GrConfigProvider`, otherwise `neutral`.
closableboolean | undefinedfalseA cross. Inside a widget it is not a button — see the section on roles below.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedA step of the control scale: a tap target of the same height as the neighbouring fields. Not set — from the group and `GrConfigProvider`, otherwise `md`.
ariaLabelstring | undefinedundefinedAn accessible name for when the label does not explain the purpose.
valueGrChipValue | undefinedundefinedThe value of the chip in a group. Outside `GrChipGroup` it is not used.
darkboolean | undefinedundefinedA fill in the tone instead of a soft surface — for a chip that has to stand out. `false` by default.
radiusGrBadgeRadius | undefinedundefinedThe shape: `round` — a pill, `semi` — a tag, `square` — a stamp. Not set — from the group and `GrConfigProvider`, otherwise `round`.
labelstring | undefinedundefinedThe 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.
selectableboolean | undefinedfalseThe chip becomes a toggle: `aria-pressed`, `Enter`/`Space`.
selectedv-modelboolean | undefinedfalseThe state of the toggle outside a group. Inside a group the value is run by the group.
removeLabelstring | undefinedundefinedThe name of the dismiss button as a whole. It overrides the one built from `label`.
removeTabindexnumber | undefinedundefinedThe `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

SlotTypeDescription
defaultanyThe label of the chip. Inside a selectable chip — phrasing content only.
iconanyAn icon before the label.

Events

EventSignatureDescription
remove(event: "remove"): void—
update:selected(event: "update:selected", value: boolean): void—

Methods

MethodsSignatureDescription
focus() => void | undefinedFocus on the chip from code. Works on an interactive chip: a plain chip is a `span` with no tab stop.
blur() => void | undefinedRemove focus from the chip.
removeElHTMLButtonElement | nullThe 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.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrChipConfigurableProps, GrChipValue, GrChipSelection } from '@feugene/granularity/components/GrChip'

Filedist/types/components/GrChip/config.d.ts

grChipConfigconst
const grChipConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrChip">;

Filedist/types/components/GrChip/defaults.d.ts

GrChipConfigurablePropsinterface
interface GrChipConfigurableProps {
    tone: GrChipTone;
    size: GrChipSize;
    radius: GrChipRadius;
    dark: boolean;
}

Filedist/types/components/shared/chipGroupContext.d.ts

GrChipValuetype
type GrChipValue = string | number | boolean;
GrChipSelectiontype
type GrChipSelection = 'single' | 'multiple';
GrChipGroupContexttype
type GrChipGroupContext = {
    selection: ComputedRef<GrChipSelection>;
    isSelected: (value: GrChipValue) => boolean;

Filedist/types/components/GrChip/grChipStyles.d.ts

GrChipTonetype
type GrChipTone = GrBadgeTone;
GrChipSizetype
type GrChipSize = GrComponentSize;
GrChipRadiustype
type GrChipRadius = GrBadgeRadius;

Filedist/types/components/GrChip/safelist.d.ts

grChipSafelistconst
const grChipSafelist: string[];

Filedist/types/components/GrChip/GrChip.vue.d.ts

GrChipPropsinterface
interface GrChipProps {
    value?: GrChipValue;
    label?: string;
    tone?: GrChipTone;
    dark?: boolean;
    size?: GrChipSize;
    radius?: GrChipRadius;
    selectable?: boolean;
    selected?: boolean;
    closable?: boolean;
    disabled?: boolean;
    ariaLabel?: string;
    removeLabel?: string;
    removeTabindex?: number;
}
GrChipEmitsinterface
interface GrChipEmits {
    (e: 'update:selected', value: boolean): void;
    (e: 'remove'): void;
}

Filedist/types/components/GrChip/index.d.ts

GrChipInstancetype
type GrChipInstance = ComponentExposed<typeof GrChipComponent>;

Accessibility

APG pattern

toggle button / option

Usage from the library prose

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.

The tones are shared with the badge, the sizes are its own

The palette of eight tones and the three radii are taken from GrBadge unchanged: a chip and a badge often stand in the same row and have to match in colour.

The size scale of a chip, however, is different. A badge is set one step smaller (--gr-control-text-2xs…md), because a label is not a target for a press; a chip is clicked, and its type size matches the neighbouring control (--gr-control-text-xs…lg). Hence, too, the explicit height a badge does not have at all.

The height is still below the control one — 24 / 28 / 32 / 36 px against 28 / 32 / 40 / 44 for an input field and a button: a chip is denser, because it travels in sets and several to a line. The practical consequence is worth knowing in advance: the ceiling of the scale is 36px, that is, a chip does not reach the 44×44 required by WCAG 2.5.5 and the Apple HIG for a target under a finger on any step. If exactly such a target is needed, take GrButton with size="lg", not a chip.

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 GrSelect with virtual;
  • 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.