A chip for labels and categories. Optionally a link, optionally removable.

Last updated

Uses JavaScript

A document-level click listener wires every removable tag’s remove button: clicking it dispatches a cancelable apogee:tag-remove event (detail: { value, tag }) on the tag. Listen for it to update your own state; set dismissOnClick to also remove the element from the DOM when the event isn’t cancelled.

Without JS: The remove button is hidden entirely — tags render as static, non-removable chips (a link tag still works, since links need no JS).

DesignEngineeringDocs
Show code
<Tag>Design</Tag>
<Tag removable value="engineering">Engineering</Tag>
<Tag href="#">Docs</Tag>

Add

npx @ava-ui/apogee add tag

No Apogee component dependencies. Depends on @lucide/astro (renders the remove icon). Ships one delegated, document-level JS listener (progressive enhancement).

Usage

---
import Tag from '@/components/apogee/Tag/Tag.astro'
---
<Tag removable value="engineering">Engineering</Tag>

API

@composable — exposed to Content Editors in AVA. Any other attribute passes through to the root <span data-apogee-tag>.

Prop Type Default Description
color 'primary' | 'neutral' | 'critical' | 'positive' | 'warning' 'neutral' The hue.
variant 'solid' | 'faded' | 'outline' 'outline' The fill style.
size 'small' | 'medium' | 'large' 'medium' Height: 24 / 28 / 32px.
rounded boolean false Fully round ends.
href string — Renders the inner label as an <a> instead of a <span>.
removable boolean false Shows a remove button (hidden without JS).
count number — A count pill after the label (tabular numbers).
selectable boolean false A native checkbox inside the chip: click or Space toggles it, the form posts name=value. Ignored with href.
checked boolean false Initial state of a selectable tag.
name string — Form field name of a selectable tag.
value string — Identifies this tag in the apogee:tag-remove event’s detail.
dismissOnClick boolean false Also removes the element from the DOM when the remove event isn’t cancelled — for cosmetic lists with no backing state.
removeLabel string 'Remove' Accessible label for the remove button.
class string — Extra classes, merged last.

Variants × colors

PlainLinkRemovableRoundedSmallLarge
Show code
<Tag>Plain</Tag>
<Tag href="#">Link</Tag>
<Tag removable value="removable">Removable</Tag>
<Tag rounded>Rounded</Tag>
<Tag size="small">Small</Tag>
<Tag size="large">Large</Tag>
PrimaryNeutralCriticalPositiveWarning
PrimaryNeutralCriticalPositiveWarning
PrimaryNeutralCriticalPositiveWarning
Show code
<Tag color="primary" variant="solid">Primary</Tag>
<Tag color="neutral" variant="solid">Neutral</Tag>
<Tag color="critical" variant="solid">Critical</Tag>
<Tag color="positive" variant="solid">Positive</Tag>
<Tag color="warning" variant="solid">Warning</Tag>

<Tag color="primary" variant="faded">Primary</Tag>
<Tag color="neutral" variant="faded">Neutral</Tag>
<Tag color="critical" variant="faded">Critical</Tag>
<Tag color="positive" variant="faded">Positive</Tag>
<Tag color="warning" variant="faded">Warning</Tag>

<Tag color="primary">Primary</Tag>
<Tag color="neutral">Neutral</Tag>
<Tag color="critical">Critical</Tag>
<Tag color="positive">Positive</Tag>
<Tag color="warning">Warning</Tag>

Count and selectable

Open12Drafts3
Show code
<Tag count={12}>Open</Tag>
<Tag color="primary" variant="solid" count={3}>Drafts</Tag>

Selectable tags are checkboxes; the chip takes the hue’s faded fill while checked:

Show code
<form class="flex flex-wrap gap-2">
<Tag selectable name="topics" value="design">Design</Tag>
<Tag selectable name="topics" value="code" checked>Code</Tag>
<Tag selectable name="topics" value="ops" color="positive">Ops</Tag>
</form>

Padding

The side that holds an icon or the remove button pads less, so the glyph sits optically inside the chip:

PlainWith iconRemovable
Solid
Show code
<Tag>Plain</Tag>
<Tag><Mail slot="start" />With icon</Tag>
<Tag removable>Removable</Tag>

States

  • Hover / focus — the label (link or plain text) and the remove button each get a visible focus-visible ring.
  • No JS — the remove button doesn’t render at all (no-js:hidden).
  • Removed — clicking remove dispatches apogee:tag-remove; the tag only leaves the DOM if your handler calls .remove() (or you set dismissOnClick).

Examples

With an icon:

Email
Show code
<Tag><Mail slot="start" /> Email</Tag>

A cosmetic list with no backing state, self-removing on click:

<Tag removable dismissOnClick>Draft</Tag>

Controlled removal — update your own state from the event instead of letting the DOM change on its own:

<script>
  document.addEventListener('apogee:tag-remove', (e) => {
    const { value } = (e as CustomEvent).detail
    // remove `value` from your state/store, then re-render
  })
</script>

Accessibility

Key Action
Tab Moves to the link (with href), the checkbox (with selectable) and the remove button, in source order.
Enter Follows the link or presses the remove button.
Space Toggles a selectable tag or presses the remove button.
  • The label renders a real <a> (when href is set) or a plain <span> — links get full keyboard and AT navigation.
  • The remove button is a real <button type="button"> with an aria-label (removeLabel, default "Remove") — pass a specific one (e.g. "Remove Engineering") when several tags are on the page.
  • No JS — the remove button is hidden entirely rather than shown non-functional; a link tag (href) still works with JS off.

Tokens

Token Used for
--border-neutral Tag border
--background-elevation-raised / --foreground-neutral Tag background / text
--foreground-neutral-faded / --foreground-neutral Remove icon default / hover color
--outline-color-focus Focus ring (label + remove button)

Do / Don’t

  • Do pass value on every removable tag — the remove event’s detail.value is how you know which one to drop from state.
  • Do write a specific removeLabel (“Remove Engineering”) when several tags share a page.
  • Don’t use Tag for a single dismissible message — that’s an Alert with a close action.
  • Don’t reach for dismissOnClick on tags backed by real data — handle apogee:tag-remove and re-render from state instead.

Content

  • Tag text is a short noun or noun phrase (one to three words), sentence case, no ending punctuation.
  • Match the label wording to the data it represents exactly — a tag is often a filter or category key, not free copy.