Tag

Tag is a compact label for a keyword, filter, person or selection. Use it for filters and selected values people can remove.

  • Beta
  • Accessibility
  • Figma Tags 57:17206
  • packages/ui/src/components/Tag

Beta until the focus indicator (C5), the dismiss-icon contrast (pending a decision) and disabled-text contrast (C8) are resolved, and the sizes are measured in a browser. Everything else on this page is implemented and tested.

Preview

Change the controls to try every supported combination. The tag is live: click it when it's a toggle, and remove it when it's removable.

Biology(4)
tsx
import { Tag } from '@nuton/ui'

<Tag
  leading={<RiBookOpenLine />}
  sublabel="(4)"
  onRemove={removeTopic}
>
  Biology
</Tag>

Installation

Availability

@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.

1. Add the packages

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react

Peer dependencies: react and react-dom 18 or later. Tag draws its dismiss icon with @remixicon/react, and you pass Remix icons to the leading slot (D1).

2. Load tokens and component styles once

tsx
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css'     // component styles (uses the tokens)

Tags use the Label/X Small and Paragraph/X Small type tokens, which read --nu-font-body (Inter). Map it to your loaded font as described on the Button page.

Usage

tsx
import { Tag } from '@nuton/ui'

export function LessonTopics({ topics, onRemove }: { topics: string[]; onRemove: (topic: string) => void }) {
  return (
    <div role="group" aria-label="Lesson topics">
      {topics.map((topic) => (
        <Tag key={topic} onRemove={() => onRemove(topic)}>{topic}</Tag>
      ))}
    </div>
  )
}
  • The default is a stroke, static tag: plain text in a 28px outlined chip. It does nothing until you pass onClick or onRemove.
  • onClick makes the tag a toggle button; pair it with active, which sets aria-pressed. Use it for filters.
  • onRemove adds the dismiss button. The component doesn't move focus after a removal, so you must.
  • Keep labels to one or two words in sentence case. Put counts in sublabel, for example “(4)”.
  • For a status people can't act on (“Completed”, “New”), use Badge instead.

Variants and sizes

Tag has one size and two appearances. What it holds (a leading icon, an avatar or a sublabel) and what it does (nothing, toggle or remove) combine freely.

Appearance × leading content

AppearanceBasicLeft iconAvatarSublabel
StrokeBiologyReadingTobi AdeyemiChemistry(4)
GrayBiologyReadingTobi AdeyemiChemistry(4)
AppearanceLookUse it
strokeWhite fill, 1px Stroke/Soft-200 borderThe default. On white and light surfaces.
grayBg/Weak-50 fill, no borderLower emphasis, such as applied filters or tags inside a bordered card.

Figma's six types (Basic, Left Icon, Avatar, Country, Brand and Company) are one leading slot. Use leadingKind="icon" (the default) for a Remix icon, which takes the state's icon colour, and leadingKind="media" for an avatar, flag or logo, which keeps its own colours and fades to 48% when disabled. An <Avatar> in the slot shrinks to 16px and shows one initial (P2). No flags or brand logos ship in @nuton/ui (D12, C15): your app supplies them.

Dismiss icon

Biology 101Year 9 chemistryExam revision

The remove button is named “Remove <label>” when the label is plain text. Pass removeLabel otherwise, or to localise it.

Toggle

Size

Biology
Basic · 28px
Chemistry(4)
Icon and sublabel · 28px
PartValueToken
Height28 (4 + 20px line + 4)--nu-space-1, Label/X Small
Side padding8; 6 on the side with a leading item (Figma 4, spacing balance); 4 on the dismiss side--nu-space-2 / --nu-space-1-5 / --nu-space-1
Border and radius1px inside stroke, radius 6--nu-radius-6
Leading item16 × 16, 4 before the label--nu-space-4
Label, sublabel, dismiss gap4 (Figma 2, spacing balance)--nu-space-1
Dismiss icon16 × 16 icon, 24 × 24 hit area--nu-space-4 / --nu-space-6
TypeLabel: Label/X Small (12/20). Sublabel: Paragraph/X Small (12/16)--nu-type-label-x-small-*

Values follow Figma's geometry (Tag [1.1] 57:17219) and are computed from the CSS. They haven't been measured in a browser yet, which the template requires before Stable. Long labels truncate with an ellipsis when the tag is narrower than its content.

States and interactions

ReadingReading
DefaultResting state, stroke and gray. Text/Sub-600 label, Icon/Soft-400 icons.
HoverShown here statically. Stroke and gray swap fills; icons darken to Sub-600. Applies only to clickable or removable tags, on devices with hover.
FocusTab to this toggle. The ring goes around the whole tag; on the remove button it surrounds the icon. Figma has no focus state, so Tag reuses Button's ring, which is below 3:1 (C5).
BiologyMaths(2)
ActiveSelected: white fill, Stroke/Strong-950 border and Text/Strong-950 for both appearances. With onClick, sets aria-pressed="true".
ReadingPhysics
DisabledBg/Weak-50 and Text/Disabled-500. The toggle and remove buttons are disabled and leave the tab order. Media in the leading slot fades to 48%.
Not applied
Hover on a static tagFigma draws Hover for every tag. Code applies it only when the tag does something, because a hover effect on an inert label suggests an action that isn't there (pending a decision).

State tokens

Each state sets the tag's component tokens. Override them on a wrapper only for a documented exception.

TokenDefault (stroke / gray)Hover (stroke / gray)ActiveDisabled
--nu-tag-bgBg/White / Bg/Weak-50Bg/Weak-50 / Bg/WhiteBg/WhiteBg/Weak-50
--nu-tag-borderStroke/Soft-200 / nonenone / Stroke/Soft-200Stroke/Strong-950none
--nu-tag-fgText/Sub-600Text/Sub-600Text/Strong-950Text/Disabled-500
--nu-tag-sublabelText/Sub-600Text/Sub-600Text/Sub-600Text/Disabled-500
--nu-tag-iconIcon/Soft-400Icon/Sub-600Icon/Strong-950Icon/Disabled-300
--nu-tag-removeIcon/Soft-400Icon/Sub-600 / Icon/Soft-400Icon/Strong-950Icon/Disabled-300
--nu-tag-focus-shadowbuttons/important-focus (Button's neutral ring, C5)

Two colours are pending a decision: the sublabel uses local Text/Sub-600 instead of the remote 01-Tokens Soft-400 it binds (following D7 and C19), and Figma's Gray hover keeps the dismiss icon at Soft-400 while Stroke hover darkens it. Code implements that hover as bound; it's a candidate for the Figma cleanup batch (D9).

Composition examples

Filter lessons

Topic tags are toggles. The selected topics also appear as removable gray tags, and a status line announces the result. Remove a filter with the mouse, or focus it and press Delete: focus moves to the next filter, then to the topic's toggle when none are left.

BiologyMaths

Showing 4 of 7 lessons

  • Cell structure and functionBiology · 12 min
  • PhotosynthesisBiology · 18 min
  • Quadratic equationsMaths · 22 min
  • Probability basicsMaths · 14 min

Add and remove topics

A field adds topics to a lesson; each one is a removable tag. When the last tag goes, focus returns to the input.

Press Enter to add a topic. Students can filter by these.

GeneticsCell biologyRevision

Keywords on a lesson

Static tags describe content. Next to them, the lesson's status is a Status badge, because it's a state, not a keyword.

CompletedPhotosynthesisPlant cellsYear 9

Navigation and semantics

Tag renders a <span>, or a <button> with onClick. It has no link mode: don't navigate from onClick, and don't wrap a tag in an <a>. For a keyword that opens a search page, use a link. Wrap related tags in an element with role="group" and an aria-label, or in a list when their number matters.

API reference

Props

Tag props
PropTypeDefaultDescription
childrenrequiredReactNode—The label (Figma Edit Text). Plain text gives the remove button its default name.
appearance'stroke' | 'gray''stroke'Figma Style: white with a Soft-200 border, or Weak-50 fill.
activeboolean—Figma State=Active (selected). With onClick, sets aria-pressed; without it, make the selection clear in the surrounding text.
disabledbooleanfalseFigma State=Disabled. Disables the toggle and the remove button.
leadingReactNode—16px leading content: a Remix icon, an <Avatar>, a flag or a logo. Rendered aria-hidden.
leadingKind'icon' | 'media''icon'icon is tinted with the state's icon colour; media keeps its own colours and fades to 48% when disabled.
sublabelReactNode—Secondary text after the label, such as "(4)". Part of the accessible name.
onClick(event: MouseEvent<HTMLButtonElement>) => void—Makes the tag a button covering the whole tag. Combine with active for a toggle.
onRemove() => void—Figma Dismiss Icon=On. Shows the remove button, activated by click, Enter, Space, Delete or Backspace. You move focus afterwards.
removeLabelstring"Remove <label>"Accessible name of the remove button. The default needs a plain-text label (development warning otherwise). Pass a localised string.
refRef<HTMLSpanElement>—Forwarded to the outer span.
…restOmit<HTMLAttributes<HTMLSpanElement>, 'onClick'>—Any other attribute for the outer span (id, title, data-*, aria-*…). onClick is replaced by the prop above.

Styling hooks

The component renders .nu-tag with data-appearance, data-active, data-disabled, data-interactive, data-leading (the leading kind) and data-removable. Its parts are .nu-tag__main, .nu-tag__leading, .nu-tag__label, .nu-tag__sublabel and .nu-tag__remove. Use props rather than styling these. The --nu-tag-* tokens (see State tokens) are the supported override points.

Accessibility

Keyboard

KeyBehaviour
TabMoves to the toggle (with onClick), then to the remove button (with onRemove). Static tags aren’t focusable; disabled ones are skipped.
Enter / SpaceToggles the tag, or removes it when the remove button has focus.
Delete / BackspaceRemoves the tag when either its toggle or its remove button has focus.

Screen readers

A static tag is plain text. A toggle is a button with aria-pressed, named by the label and sublabel. The remove button is named “Remove <label>” or by removeLabel. The leading item is hidden. Announce filter results in a status region.

Focus

The ring is Button's neutral focus effect, below 3:1 today (C5); Figma defines none for Tag. Browsers without :has() show the native outline on the toggle. Tag doesn't move focus after a removal.

Contrast and colour

Labels pass: Sub-600 is 6.69:1 on white and 6.24:1 on Weak-50; active Strong-950 is 17.93:1. The dismiss and leading icons (Icon/Soft-400) measure 2.49:1 and 2.33:1, below 3:1 (WCAG 1.4.11, pending a decision). Disabled text is 1.43:1 (C8). Active is also exposed as aria-pressed; in forced colours it gains an outline.

Touch and pointer

The toggle covers the whole 28px tag. The remove button's 16px icon has a 24 × 24 hit area (WCAG 2.5.8) that sits above the toggle. Hover applies only on hover-capable pointers and only to tags that do something.

You must: move focus to the next tag or the input after a removal; pass removeLabel when the label isn't plain text or the UI isn't in English; give a group of toggle tags an accessible name; use a link, not a tag, for navigation.

Figma mapping

Figma propertyFigma valuesCode
TypeBasic · Left Icon · Avatar · Country · Brand · Companyleading + leadingKind: icon (Left Icon) · media (the rest)
🏵️ StyleStroke · Grayappearance
📌 StateDefault · Hover · Active · DisabledInteractive hover, active, disabled, plus focus (code-only)
✖️ Dismiss IconOff · OnonRemove (and removeLabel)
SublabelBoolean + textsublabel
Pick Left / Country / Brand / CompanyInstance swapsThe node you pass to leading
No propertyNoneonClick: toggle behaviour (code-only)

Guidelines and related components

Use Tag for

  • Filters people switch on and off, such as lesson topics.
  • Values someone has picked and can remove: topics, spaces, students.
  • Short keywords that describe content.

Don't use Tag for

  • A status or count people can't act on. Use Badge.
  • Choosing exactly one option from a small set. Use Button group or Radio.
  • Navigation. Use a link.

Do and don't

GeneticsCell biology
DoKeep labels to a word or two. Tags are scanned in rows; short labels keep the row readable.
Lessons about how cells divide and grow
Don'tDon't write sentences in tags. Long labels truncate and turn a row of tags into a paragraph.
Chemistry(4)
DoPut counts in the sublabel. The label stays the name; the count reads as secondary.
Chemistry 4 lessons
Don'tDon't run the count into the label. “Chemistry 4 lessons” is harder to scan and to sort.
Completed
DoUse a status badge for status. A state people can't change isn't a tag.
Completed
Don'tDon't show status as a tag. It looks selectable or removable, and it hides the status icon.
PhysicsMaths
DoUse one appearance per group. Tags in a row read as a set of equals.
PhysicsMaths
Don'tDon't mix appearances in one row. People read the difference as meaning, such as selected and unselected.

Common mistakes

  • Removing a tag and leaving focus on nothing, so keyboard users are sent back to the top of the page.
  • Using active on a static tag with nothing else to show the selection.
  • Passing a non-text label (an element) to a removable tag without removeLabel, so the button is just “Remove”.
  • Changing the results of a filter without announcing them in a status region.