TL;DR
A tag component displays a compact piece of information, but its appearance does not tell you whether it should be interactive. Use plain text for categories and status, a toggle button for a selectable filter, and a separate removal control for an editable token. In Vue, keep the label data distinct from selected-filter state, then verify contrast, keyboard behavior, overflow, and the filtered result.
A dashboard row might say Pending, a search result might carry Analytics, and a filter bar might offer Analytics as a choice. Those labels can share a visual language without sharing the same behavior. The reliable approach is to choose the job first and style the resulting semantic element second. Carbon’s tag guidelines distinguish read-only, dismissible, selectable, and operational variants, while the GOV.UK Design System deliberately reserves its tag for noninteractive status. The names differ across systems; the interaction contract is what matters.
This guide builds a small Vue 3 dashboard example with read-only tags and a working filter bar. We will also decide when a form needs a tag-entry control instead.
Table of contents
- Before you start
- 1. Assign one job to each tag
- 2. Set the label vocabulary and layout rules
- 3. Build read-only tags and selectable filters in Vue
- 4. Keep editable tokens separate from display tags
- 5. Check semantics, contrast, and responsive behavior
- 6. Test the finished tag component in a real screen
- Frequently asked questions
- Put the pattern into your application
- Sources
- Recommended Reads
Before you start
Have a Vue 3 project that supports Single-File Components and an application view with category and status data. No DOM Studio package is required for the standalone code below. You need permission to edit the relevant view and its styles; if the results come from an API, identify where the current filter state and server query should live before adapting the example. A browser, keyboard, narrow viewport, and contrast checker will help with the final review.
For our example, a record has an ID, name, category, and status. Categories are selectable filters; statuses describe rows. We will use a short, fixed list to keep the demonstration focused. Replace the sample records with your application’s data and access rules, not just its colors.
1. Assign one job to each tag
Write down what happens when someone clicks the label. If the answer is “nothing,” it should not acquire button behavior or a tab stop. If it changes a local result set, make the whole filter an obvious button. If it removes a selected value, make the remove action explicit. Carbon’s tag variants provide a useful behavior reference, although you do not need to adopt its exact names.
| Job in the interface | Example | Suitable starting point |
|---|---|---|
| Identify an attribute | Enterprise on an account row | Read-only text tag |
| Report a state | Pending on a task | Read-only status tag |
| Narrow results | Enterprise above the account table | Toggle button with a selected state |
| Remove a chosen value | Enterprise inside an edit form | Text token plus an independently named remove button |
| Add or search for values | Entering recipients or categories | Tag-entry field or combobox |
This decision also resolves the badge, chip, and pill naming problem. A badge usually draws attention to a small fact or count; chip and pill often describe shape, not semantics. A button is an action, and a dropdown opens a set of choices. Teams may call all of these “tags,” but they must not make a read-only status look clickable or make an active filter look like inert decoration. The GOV.UK system’s status-only guidance is a good reminder that visual similarity does not grant interaction.
Check: next to every proposed tag, write “reads,” “toggles,” “removes,” “opens,” or “edits.” If one tag has two verbs, separate the controls or simplify the task. If users are expected to navigate to another page, use a recognizable link rather than silently turning a status lozenge into navigation.

2. Set the label vocabulary and layout rules
Good tag text survives scanning out of context. Prefer Needs review over an unexplained abbreviation, and keep status wording consistent across table rows, details, and filters. For status, descriptive words or short phrases generally read more clearly than action verbs: Awaiting review describes state, while Review can sound like a command. Carbon recommends concise, informative tag titles; the exact character budget should follow your data rather than force misleading abbreviations.
Define the data contract before choosing a palette. A category might have { id, label }; a status might have a stable code and display label. Keep the stable identifier for filtering and sorting, and use the human-readable label in the UI. Establish one capitalization style, one priority order for statuses, and a predictable order for categories. For an AI-classified record, show a meaningful label only after the classification is available and validated; do not quietly replace “Unknown” with an optimistic state.
Long labels need a deliberate escape route. Let a group wrap on narrower screens, but avoid breaking a short tag internally. For uncontrolled values such as customer-defined categories, decide whether the full text should wrap, be revealed in adjacent details, or be shortened visually with an accessible full label. A hover-only tooltip does not help touch or keyboard users. A dense table might show one or two tags and an explicit Show all categories control rather than squeeze eight unreadable pills into a cell.
Check: try a two-word status, a 35-character user-defined label, an empty value, and a translated label. If the UI shows only color or an ellipsis with no way to learn the full value, the content contract is unfinished.
The video below walks through chip appearance in Figma. Treat it as a visual-design companion, then use the semantic and keyboard checks in this guide for the actual web controls.
3. Build read-only tags and selectable filters in Vue
The following Single-File Component can sit in an existing Vue 3 application. It uses only Vue’s ref and computed APIs. The sample data is local; authentication, API loading, pagination, URL synchronization, and server-side filtering are intentionally outside this example. Vue documents v-for keys and computed filtered lists, while the W3C button pattern documents aria-pressed for stable-label toggles and activation by Enter or Space.
<script setup>
import { computed, ref } from 'vue'
const categories = [
{ id: 'analytics', label: 'Analytics' },
{ id: 'billing', label: 'Billing' },
{ id: 'support', label: 'Support' }
]
const records = ref([
{ id: 't1', name: 'Usage report', category: 'analytics', status: 'Ready' },
{ id: 't2', name: 'Invoice review', category: 'billing', status: 'Pending' },
{ id: 't3', name: 'Customer reply', category: 'support', status: 'Ready' }
])
const selectedCategories = ref([])
const visibleRecords = computed(() =>
selectedCategories.value.length === 0
? records.value
: records.value.filter(record =>
selectedCategories.value.includes(record.category)
)
)
function toggleCategory(id) {
selectedCategories.value = selectedCategories.value.includes(id)
? selectedCategories.value.filter(selected => selected !== id)
: [...selectedCategories.value, id]
}
function categoryLabel(id) {
return categories.find(category => category.id === id)?.label ?? 'Uncategorized'
}
</script>
<template>
<section aria-labelledby="work-heading">
<h2 id="work-heading">Work items</h2>
<fieldset class="filter-group">
<legend>Filter by category</legend>
<div class="filter-list">
<button
v-for="category in categories"
:key="category.id"
type="button"
class="filter-chip"
:class="{ 'filter-chip--selected': selectedCategories.includes(category.id) }"
:aria-pressed="selectedCategories.includes(category.id)"
@click="toggleCategory(category.id)"
>
{{ category.label }}
</button>
</div>
</fieldset>
<p aria-live="polite">{{ visibleRecords.length }} records shown.</p>
<table>
<caption>Work items by category and status</caption>
<thead>
<tr><th scope="col">Item</th><th scope="col">Category</th><th scope="col">Status</th></tr>
</thead>
<tbody>
<tr v-for="record in visibleRecords" :key="record.id">
<th scope="row">{{ record.name }}</th>
<td><span class="tag">{{ categoryLabel(record.category) }}</span></td>
<td>
<span class="tag" :class="record.status === 'Ready' ? 'tag--ready' : 'tag--pending'">
{{ record.status }}
</span>
</td>
</tr>
</tbody>
</table>
<p v-if="visibleRecords.length === 0">No items match these filters. Choose another category.</p>
</section>
</template>
<style scoped>
.filter-list { display: flex; flex-wrap: wrap; gap: .5rem; }
.filter-chip { min-height: 2rem; padding: .25rem .75rem; border: 2px solid #465775; border-radius: 999px; background: #fff; color: #18253a; cursor: pointer; }
.filter-chip--selected { background: #18253a; color: #fff; }
.filter-chip:focus-visible { outline: 3px solid #264db8; outline-offset: 3px; }
.tag { display: inline-block; max-width: 100%; overflow-wrap: anywhere; padding: .2rem .6rem; border-radius: 999px; background: #e6ebf2; color: #18253a; }
.tag--ready { background: #dcefe7; }
.tag--pending { background: #fff0d4; }
</style>
Expected result: the table starts with three records; selecting Billing shows one. Pressing the same filter again restores all three. The text-only category and status tags never enter the tab order; filter buttons do. The count changes when the result changes, and an empty response has visible copy.
Troubleshooting: if the filtered rows do not update, check that your real record’s category stores the same stable IDs used by the buttons. If the table uses server data, send the selected IDs to the server and handle loading, failure, and stale responses; a local computed filter alone does not filter records that have not been loaded. The CSS colors are illustrative, not an audited accessible palette for your theme.

4. Keep editable tokens separate from display tags
A removable tag has two things to communicate: the selected value and the action to remove it. Use a distinct button with a name such as Remove Analytics; do not make a whole status tag clickable just because another screen lets someone edit its category. On removal, decide where focus goes when that button disappears. Returning it to the tag-entry field or a nearby surviving control is more predictable than dropping focus into the page.
If someone must search approved tags, paste multiple values, or create new ones, use a tag-entry control instead of extending the table’s read-only span. DOM Studio’s Tag Combobox documents selected values, known options, optional custom entries, and its keyboard behavior for a form field. Its example also gives a remove button an accessible name. That component solves entering and managing values, not displaying every category or filtering every dashboard table. Keep these responsibilities separate so a future change to form validation does not rewrite the display tag.
In a settings form, the selected tokens might represent notification topics and need removal. In a search toolbar, a selected-filter summary might also offer a remove action, but it should reflect the same selection state as the filter buttons, not become an independent copy. When a token is disabled or removal is not permitted, do not display an actionable-looking close icon that does nothing.
Check: use only the keyboard to add a value, find its remove control, remove it, and continue editing. Confirm the accessible name includes the specific value and that focus has a sensible next location.
5. Check semantics, contrast, and responsive behavior
A tag can be tiny visually while its information remains important. For ordinary tag text, WCAG 2.2 text contrast guidance specifies at least 4.5:1 for normal-size text. When an outline or other graphical cue is necessary to recognize a filter or its selected state, non-text contrast guidance calls for 3:1 against adjacent colors. Test the actual theme and states; do not assume the sample CSS or a brand palette passes. Status should say Ready or Pending as text rather than rely on green versus yellow alone, as the GOV.UK tag guidance emphasizes.
Interactive filters need visible focus, a stable button label, and a programmatic selected state. Native <button> elements already support keyboard activation, so the example needs no custom keydown handler. The W3C button pattern describes aria-pressed when the same label toggles on and off. A group legend gives the controls context. If choosing one option is mandatory in a submitted form rather than filtering a view, consider radio-button semantics instead of recycling this independent multi-filter pattern.
Do not compress a remove icon into an unreasonably small target. WCAG 2.2’s Target Size (Minimum) sets a 24 by 24 CSS-pixel minimum for pointer targets, with stated exceptions including sufficient spacing. Use a comfortably sized hit area even when the visible icon is smaller. Also inspect focus outlines at 200% zoom, narrow widths, and with a long translated label: a filter group should wrap without trapping focus or covering its neighbors.
Check: tab through filters and any remove controls, activate each with Space and Enter, and listen for selected state with a screen reader. Switch to a high-contrast or custom theme and compare status text, button outlines, and focus cues. If the controls are distinguishable only by hue, adjust the design rather than adding more color variants.
6. Test the finished tag component in a real screen
Move from the isolated example into one dashboard, table, or search view. The same category can appear as a static attribute in a row and as a filter button above it; that is the main acceptance test for your component boundary. Test a form separately if it uses tag entry.
- Data and ordering: verify stable IDs, duplicate categories, unknown statuses, empty values, and the expected order after a refresh.
- Filtering: select two categories, deselect one, clear the remaining filter, and confirm the count and rows agree. Check a zero-result state and an API failure if results are remote.
- Keyboard and assistive technology: make sure read-only tags are not focusable; verify the announced filter label and pressed state; check that removing a token does not lose focus.
- Layout and content: test long user-defined tags, wrapping rows, table overflow, mobile widths, zoom, translation, and disabled actions.
- Visual states: check contrast for text, selected outlines, focus, and hover against each supported theme. Do not use disabled styling to conceal an unclear business rule.
If a label overflows only in the table, fix the table’s content policy rather than making every tag globally truncate. If a filter button works with a mouse but not by keyboard, inspect whether it is actually a native button. If two controls change the same selected category but disagree afterward, move their selection into one parent-owned state.
Frequently asked questions
Should a read-only status tag use aria-pressed?
No. aria-pressed describes a toggle button’s on or off state, not the text of a status. Render the status as readable text. Reserve a button and pressed state for a user-operated filter, as described in the W3C button pattern.
When should I use a tag combobox instead of a filter tag?
Use a filter tag when someone selects from visible choices to narrow the current view. Use a tag combobox when they must find, enter, or remove values in a field. DOM Studio’s Tag Combobox documentation shows the form-entry behavior; the Vue example here covers the dashboard filter.
Put the pattern into your application
You now have a working choice of markup for each tag job and a small Vue filter example to adapt. Start with one screen where a category appears in both a table and a toolbar, test the distinct behaviors, then reuse the display and filter patterns only where their contracts fit. For editable form values, try the DOM Studio Tag Combobox playground rather than turning a status label into a form control.
Sources
- Carbon Design System: Tag guidelines
- GOV.UK Design System: Tag
- Vue: List Rendering
- Vue: Reactivity Fundamentals
- Vue: Computed Properties
- W3C WAI-ARIA Authoring Practices: Button Pattern
- W3C: Understanding Contrast (Minimum)
- W3C: Understanding Non-text Contrast
- W3C: Understanding Target Size (Minimum)
- DOM Studio: Tag Combobox
