Tag Selection
Mandatory rules for Status Tag, Label Tag, numeric metrics vs codes, shadcn Badge, and filter chips — no mood-based pills.
Use this page as the single source of truth when choosing a pill-shaped UI element. There is no “use whatever looks nice” path.
Examples
Status Cell
Left side picks pills by taste (Badge as status, StatusTag for provenance, LabelTag on a currency code). Right side follows the tree: StatusTag for outcome, LabelTag for Auto, plain text for CNY.
Wrong
Status cell
Badge as status · StatusTag for provenance · LabelTag on currency code
Right
Status cell
StatusTag + LabelTag for orthogonal facets; code stays plain text
import { Badge } from "@/components/ui/badge";
import { LabelTag } from "@/components/f-ui/label-tag";
import { StatusTag } from "@/components/f-ui/status-tag";
import { DesignCompare } from "@/demos/_design/design-compare";
export function TagSelectionDemo() {
return (
<DesignCompare
wrong={
<div className="space-y-3">
<p className="text-sm font-medium text-muted-foreground">
Status cell
</p>
<div className="flex flex-wrap items-center gap-1.5">
<Badge>Approved</Badge>
<StatusTag tone="success">Auto</StatusTag>
<LabelTag accent="accent-1">CNY</LabelTag>
</div>
<p className="text-xs text-muted-foreground">
Badge as status · StatusTag for provenance · LabelTag on currency code
</p>
</div>
}
right={
<div className="space-y-3">
<p className="text-sm font-medium text-muted-foreground">
Status cell
</p>
<div className="flex flex-wrap items-center gap-1.5">
<StatusTag tone="success">Approved</StatusTag>
<LabelTag accent="neutral">Auto</LabelTag>
<span className="text-sm">CNY</span>
</div>
<p className="text-xs text-muted-foreground">
StatusTag + LabelTag for orthogonal facets; code stays plain text
</p>
</div>
}
/>
);
}Filter Chips
Active filters are removable conditions — not read-only field values. Left side wraps StatusTag / LabelTag with an ×; right side uses the filter-bar chip surface.
Wrong
Active filters
StatusTag / LabelTag cosplay as removable filter chips
Right
Active filters
Filter bar chips remove conditions — never LabelTag or StatusTag
import { X } from "lucide-react";
import { DataListFilterTag } from "@/components/f-ui/data-list-chrome/data-list-filter-tag";
import { LabelTag } from "@/components/f-ui/label-tag";
import { StatusTag } from "@/components/f-ui/status-tag";
import { DesignCompare } from "@/demos/_design/design-compare";
export function TagSelectionFilterChipDemo() {
return (
<DesignCompare
wrong={
<div className="space-y-3">
<p className="text-sm font-medium text-muted-foreground">
Active filters
</p>
<div className="flex flex-wrap items-center gap-1.5">
<button
type="button"
className="inline-flex items-center gap-1"
aria-label="Remove status filter"
>
<StatusTag tone="success">Status is Approved</StatusTag>
<X className="size-3 text-muted-foreground" />
</button>
<button
type="button"
className="inline-flex items-center gap-1"
aria-label="Remove channel filter"
>
<LabelTag accent="accent-1">Channel is Web</LabelTag>
<X className="size-3 text-muted-foreground" />
</button>
</div>
<p className="text-xs text-muted-foreground">
StatusTag / LabelTag cosplay as removable filter chips
</p>
</div>
}
right={
<div className="space-y-3">
<p className="text-sm font-medium text-muted-foreground">
Active filters
</p>
<div className="flex flex-wrap items-center gap-1.5">
<DataListFilterTag
label="Status is Approved"
removeAriaLabel="Remove Status is Approved"
onRemove={() => {}}
/>
<DataListFilterTag
label="Channel is Web"
removeAriaLabel="Remove Channel is Web"
onRemove={() => {}}
/>
</div>
<p className="text-xs text-muted-foreground">
Filter bar chips remove conditions — never LabelTag or StatusTag
</p>
</div>
}
/>
);
}List And Detail Parity
The same enum field must render with the same tag component on list and detail. Left side mixes LabelTag and Badge for Role; right side keeps LabelTag on both.
Wrong
Orders list · Role
FinanceOrder detail · Role
FinanceSame field: LabelTag on list, Badge on detail
Right
Orders list · Role
FinanceOrder detail · Role
FinanceSame field uses LabelTag on list and detail
import { Badge } from "@/components/ui/badge";
import { LabelTag } from "@/components/f-ui/label-tag";
import { DesignCompare } from "@/demos/_design/design-compare";
export function TagSelectionListDetailParityDemo() {
return (
<DesignCompare
wrong={
<div className="space-y-3">
<div className="space-y-1.5">
<p className="text-xs text-muted-foreground">Orders list · Role</p>
<LabelTag accent="neutral">Finance</LabelTag>
</div>
<div className="space-y-1.5 border-t pt-3">
<p className="text-xs text-muted-foreground">Order detail · Role</p>
<Badge>Finance</Badge>
</div>
<p className="text-xs text-muted-foreground">
Same field: LabelTag on list, Badge on detail
</p>
</div>
}
right={
<div className="space-y-3">
<div className="space-y-1.5">
<p className="text-xs text-muted-foreground">Orders list · Role</p>
<LabelTag accent="neutral">Finance</LabelTag>
</div>
<div className="space-y-1.5 border-t pt-3">
<p className="text-xs text-muted-foreground">Order detail · Role</p>
<LabelTag accent="neutral">Finance</LabelTag>
</div>
<p className="text-xs text-muted-foreground">
Same field uses LabelTag on list and detail
</p>
</div>
}
/>
);
}Status Stack
Stacking is allowed only for orthogonal facets. Left side invents an Approved Auto mega-status plus a second StatusTag; right side keeps one StatusTag for outcome and a LabelTag for provenance.
Wrong
Status cell
Mega-status string + twin semantic StatusTags
Right
Status cell
ApprovedAutoOne StatusTag for outcome + LabelTag for provenance
import { LabelTag } from "@/components/f-ui/label-tag";
import { StatusTag } from "@/components/f-ui/status-tag";
import { DesignCompare } from "@/demos/_design/design-compare";
export function TagSelectionStatusStackDemo() {
return (
<DesignCompare
wrong={
<div className="space-y-3">
<p className="text-sm font-medium text-muted-foreground">
Status cell
</p>
<div className="flex flex-wrap items-center gap-1.5">
<StatusTag tone="success">Approved Auto</StatusTag>
<StatusTag tone="info">System</StatusTag>
</div>
<p className="text-xs text-muted-foreground">
Mega-status string + twin semantic StatusTags
</p>
</div>
}
right={
<div className="space-y-3">
<p className="text-sm font-medium text-muted-foreground">
Status cell
</p>
<span className="inline-flex max-w-full items-center gap-1.5 whitespace-nowrap">
<StatusTag tone="success">Approved</StatusTag>
<LabelTag accent="neutral">Auto</LabelTag>
</span>
<p className="text-xs text-muted-foreground">
One StatusTag for outcome + LabelTag for provenance
</p>
</div>
}
/>
);
}Principles
- Read-only display of status or category → only Status Tag or Label Tag.
- Read-only display of currency codes or id fragments → plain text (no pill, left-align).
- Read-only display of comparable magnitudes (amount, qty, tax rate %, margin %) →
f.number/f.currency(right-align + tabular-nums; never LabelTag). - Interactive (select, remove, navigate) → only shadcn
Badgeinside controls, or table filter chips — never Label Tag. - Numeric overlay on an icon or avatar (unread count) → Count Badge; do not fake it with Tag or Badge text.
Decision Tree
Where does this pill appear?
├─ Table cell or detail field showing a stored value?
│ ├─ Good/bad/urgent/processing semantics? → StatusTag (f.enum({ render: "status" }))
│ ├─ Category, role, channel, type? → LabelTag (f.enum({ render: "label" }))
│ ├─ Comparable magnitude (amount, qty, tax rate %, margin %)? → f.number / f.currency (right-align)
│ └─ Currency code / id fragment? → Plain text (left-align, no pill)
│
├─ Inside an input (Multi-Select selected items)?
│ └─ shadcn Badge (removable chip) — do not use LabelTag
│
├─ Active filter with remove (×) on the filter bar?
│ └─ DataTableFilterBar chip — do not hand-roll Badge or LabelTag
│
└─ Unread count on Bell / avatar?
└─ Count Badge (numeric overlay) — never Status Tag / Label Tag / shadcn BadgeHard Ban — Mood Pills
Developers must not pick Status Tag vs Label Tag by visual taste. Every table/detail enum goes through the tree above. Reviewers reject PRs that:
- Put
success/warning/destructivetones on category fields - Wrap currency codes in Label Tag / Status Tag
- Wrap tax rate / margin % in Label Tag (use numeric column instead)
- Left-align tax rate / margin % / amount as if they were prose
- Use shadcn
Badgefor read-only status or category cells - Use Status Tag for values that never mean good / bad / urgent / in-flight
Scenario Matrix
| Scenario | Component | Notes |
|---|---|---|
| Order status, payment, priority | Status Tag | f.enum({ render: "status" }) with tone |
| In progress with spinner | Status Tag | icon: "spin" on that variant only |
| Completed / failed with icon | Status Tag | icon: "auto" — opt-in, not table default |
Offer / contract lifecycle (Approved, Effective) | Status Tag | Outcome / window state |
| Category, channel, role, type | Label Tag | f.enum({ render: "label" }); default neutral |
| Dense table columns | Status / Label Tag | Omit icon unless the state needs emphasis |
| Multi-Select selected option | shadcn Badge | Full pill, removable — component-internal |
| Filter bar active condition | Filter bar chip | Part of list filter surfaces |
| Detail page role (read-only) | Label Tag | Same as users table — not shadcn Badge |
| Marketing / link pill | shadcn Badge | asChild + link OK; not for table enums |
Sidebar / bell unread 3 | Count Badge | Numeric overlay — not Tag; host control owns the accessible name |
| Currency ISO code (CNY/USD) | Plain text (left) | Code / identifier — not a magnitude |
| Tax rate / Margin % | f.number (right + tabular-nums) | Comparable metric; put % in the header (Tax rate %), not a LabelTag |
Status Tag vs Label Tag vs Plain Text
| Question | Yes → | No → |
|---|---|---|
| Should the user care if the value is good, bad, urgent, or in-flight? | Status Tag | Continue |
| Is it a category / role / channel / type that needs scan chips? | Label Tag (no semantic tones) | Continue |
| Is it a magnitude users compare across rows (amount, qty, tax %, margin %)? | f.number / f.currency (right-align) | Continue |
| Is it a currency code, unit label, or id fragment? | Plain text (left-align) | — |
Schema (via Field Types):
f.enum({ render: "status" })variants:{ label, tone, icon? }—tonerequired.f.enum({ render: "label" })variants:{ label, accent? }— notone; optionalaccent: "auto".- Measures / tax rate % / margin % →
f.number(right-align); currency amounts →f.currency; currency codes →f.text. - Do not fake percentages as LabelTag pills.
See Field Types and Table for schema-driven status and label rendering.
Label Tag vs shadcn Badge
Both can look like small capsules. They are not interchangeable.
| Label Tag | shadcn Badge | |
|---|---|---|
| Job | Show a field value | Part of a control (selection, link) |
| Shape | rounded-md + visible border | rounded-4xl pill |
| Interaction | Static | Often removable / clickable |
| Color | Non-semantic accent or neutral | primary, secondary, outline, … |
| Import | @/components/f-ui/label-tag | @/components/ui/badge |
Visual rule of thumb: bordered, squarer, in a table cell → data tag. Full pill inside an input with × → shadcn Badge.
Do not display the same field two ways (e.g. Label Tag in the table and Badge on the detail page for role).
Multiple Pills In One Status Cell
Stacking more than one pill under a Status column is allowed when each pill answers a different question — industry default, not a smell.
| Precedent | Split |
|---|---|
| SAP Fiori Object Status vs Object Marker | Business lifecycle (Object Status) ≠ technical / marker state (Object Marker). Rare dual statuses: primary emphasized, secondary muted. |
| GitHub Issues / PRs | State pill ≠ Label pills on the same row. |
| Ant Design Table | Many Tags in a Tags column = categories; Status demos stay one semantic Tag. |
When To Stack
| Primary | Secondary | Example |
|---|---|---|
| Status Tag (outcome / lifecycle) | Label Tag (provenance / category) | Approved + Auto when decisionSource=system |
| Status Tag | Omit secondary | Human-approved line — no Manual chip (noise) |
Aligns with Domain Status Vs Derived View: outcome ⊥ provenance (do not invent Approved Auto as one StatusTag).
Hard Rules
- One primary StatusTag for business outcome — leftmost.
- Secondary = LabelTag (or future marker), not a second semantic StatusTag fighting for attention.
- Default at most one secondary chip in the Status cell; more than one needs an explicit product reason (SAP markers cap ~3 technical markers).
- Prefer a shared cell helper (compose once) — do not scatter ad-hoc double tags.
- Do not use StatusTag
icon="auto"to mean “system decided” — that API is an opt-in status glyph, not provenance. Use LabelTagAuto(or dedicated column) for provenance. - Category piles (
bug,p1, channel, …) belong in a Labels / category column, not under Status.
Layout
<span className="inline-flex max-w-full items-center gap-1.5 whitespace-nowrap">
<StatusTag tone="success">Approved</StatusTag>
<LabelTag accent="neutral">Auto</LabelTag>
</span>Sort / filter by the lifecycle field; “Auto approved” filters = outcome ∩ provenance (not a third enum).
What We Do Not Ship
| Component | Why |
|---|---|
| f-ui Badge for status/labels | Overlaps Status Tag / Label Tag; confuses variant vs tone |
| Label Tag for filters | Use filter bar or Multi-Select |
| shadcn Badge for table status/category columns | Use f.enum({ render: "status" }) / f.enum({ render: "label" }) |
| Any pill for currency code or tax rate | Codes = plain text; tax rate = numeric column |
Code Review Checklist
- Table/detail enum uses
f.enum({ render: "status" })orf.enum({ render: "label" }), not legacybadge. - No
import { Badge } from "@/components/ui/badge"for read-only status, category, or role. - Label fields do not use semantic status tones.
- Currency codes are plain text; tax rate / margin % use
f.number(right-align), not Tag. - Processing states use
icon: "spin"only where the value is truly in-flight. - Same field uses the same tag component on list and detail views.
- Status cell stacks StatusTag + LabelTag only for orthogonal facets (e.g. outcome + Auto); no twin semantic StatusTags; no
Approved Automega-status. - StatusTag
icon="auto"is not used as a substitute for provenance LabelTagAuto.
Related
- Field Types —
f.enumauthoring for status and label columns - Status Tag — API and tone presets
- Label Tag — API and accents
- Table — schema-driven rendering
- Domain Status Vs Derived View — outcome ⊥ provenance
- Row Actions Column — omit empty Actions chrome
- Approval And Case Patterns — submit vs approve seats
Internationalization
How f-ui separates app i18n from component strings—I18nProvider, resolver hooks, bundles, and host translators.
Row Actions Column
How to design table row Actions — identity Open, icon chrome, hide vs disable, trailing Delete, danger-only / multi-danger, and overflow when a row has many ops.