Badge
A small, non-interactive label that attaches a fact — duration, category, status, count — to something else.
When to Use
Variants
Types
Six variants. category takes a custom color; status has no fill of its own and takes its colour from the status key (info when none is given); the rest are self-contained.
<?= ws_badge('Default') ?>
<?= ws_badge('45 min', ['variant' => 'duration', 'icon' => 'schedule']) ?>
<?= ws_badge('Ideation', ['variant' => 'category', 'color' => '#F59E0B']) ?>
<?= ws_badge('NEW', ['variant' => 'new']) ?>
<?= ws_badge('3', ['variant' => 'count']) ?>Status
Four semantic colours (success, warning, error, info) plus gold, the premium marker. Reach for the ws_status() shorthand rather than assembling the variant and status keys by hand.
<?= ws_status('Completed', 'success') ?>
<?= ws_status('Pending', 'warning') ?>
<?= ws_status('Failed', 'error') ?>
<?= ws_status('In Review', 'info') ?>AI-made
'variant' => 'ai' is the label for work the AI prepared: the AI mark and "Prepared by Workshopr" on the brand tint. Pass an empty string and it says that for you; pass your own words when a surface needs them, as long as they still say who made it. It takes no other icon.
<?= ws_badge('Prepared by Workshopr', ['variant' => 'ai']) ?>From JavaScript, WsAgent.aiBadge(text, options) in /js/ws-agent.js prints the same markup.
Sizes
<?= ws_badge('Small', ['size' => 'sm']) ?>
<?= ws_badge('Large', ['size' => 'lg']) ?>Pill
Swaps the default radius for a full round. Pick one shape per surface and hold it — mixed radii in the same meta row read as a bug.
<?= ws_badge('Rounded', ['pill' => true]) ?>Shorthand helpers
Three convenience wrappers cover the cases that appear most. Each just calls ws_badge() with fixed options, so anything below applies to them too.
| Helper | Equivalent to |
|---|---|
ws_duration($text, $withIcon = true) | ws_badge($text, ['variant' => 'duration', 'icon' => 'schedule', 'size' => 'sm']) — pass false to drop the icon |
ws_category($name, $color = null) | ws_badge($name, ['variant' => 'category', 'color' => $color]) |
ws_status($text, $status = 'info') | ws_badge($text, ['variant' => 'status', 'status' => $status]) |
<?= ws_duration('45 min') ?>
<?= ws_category('Strategy', '#3B82F6') ?>
<?= ws_status('Completed', 'success') ?>States
| State | Behavior |
|---|---|
| Default | The only state. Badges are static <span>s — no hover, focus, active, or disabled treatment. |
| Inside an interactive parent | A badge in a hoverable card inherits nothing; the parent owns the hover. The --transition-interactive token is present for parent-driven colour changes. |
| Empty text | Renders an empty badge — a visible coloured sliver. Guard against empty strings at the call site. |
Real-World Usage
A library card's meta row: category from the database with its stored colour, duration via the shorthand, and a status badge only when the item needs attention.
<div class="card-meta">
<?= ws_category($ex['category_name'], $ex['category_color']) ?>
<?= ws_duration($ex['duration'] . ' min') ?>
<?php if ($ex['is_new']): ?><?= ws_status('New', 'info') ?><?php endif; ?>
</div>Options
| Option | Type | Default | Purpose |
|---|---|---|---|
$text | string | '' | Positional label. Keep it to one or two words. |
variant | string | 'default' | default | duration | category | status | count | new | ai |
size | string | 'md' | sm | md | lg |
icon | string | null | Material icon rendered before the text. Not auto-selected — even duration needs it passed explicitly (the ws_duration() shorthand does this for you). |
color | string | null | Hex colour for the category variant, applied inline |
status | string | null | success | warning | error | info | gold. Adds the status colour class. With variant: 'status' that colour is the whole look; on another variant it replaces the background and text colour but keeps that variant's border. ws_status() is the shorthand. |
pill | bool | false | Full border-radius |
id | string | null | Element ID |
class | string | '' | Additional CSS classes |
attrs | array | [] | Extra HTML attributes as key/value pairs |
Accessibility
| Concern | Behavior |
|---|---|
| ARIA | None applied. The badge is a text <span> in the document flow, so its content is announced in reading order along with whatever it annotates. |
| Keyboard | Not focusable. If a badge ever needs to be actionable, that's a Button. |
| Colour independence | The label always carries the meaning in words — "Completed", not a bare green dot. Preserve that: don't ship a status badge whose text is generic while only the colour distinguishes it. |
| Context for counts | The count variant renders a bare number, which is meaningless alone to a screen-reader user. Put the noun in adjacent text or an aria-label on the parent — "3 unread notes", not "3". |
| Contrast | Built-in variants pair tested token foreground/background. A custom color on the category variant bypasses that — check it before shipping. |
Tokens
| Token | Used for |
|---|---|
--badge-bg / --badge-color / --badge-radius | Base fill, text, and corner radius |
--gray-100 / --gray-200 / --gray-500 / --gray-600 | Default and duration variant neutrals |
--brand | new and count variant fill |
--color-success-light / --color-success-dark | Success status |
--color-warning-very-light / --color-warning-light / --color-warning-ink / --color-warning-darker | Warning status |
--color-error-light / --color-error-dark | Error status |
--color-info-light / --color-info-dark | Info status |
--text-inverse | Text on solid fills |
--radius-full | Pill shape and the circular count badge |
--text-micro / --text-caption / --text-meta / --text-small / --text-body | Type scale across sizes |
--font-body / --font-medium / --font-semibold | Face and weight |
--transition-interactive | Colour transition when a parent changes state |
CSS Classes
| Class | Purpose |
|---|---|
.ws-badge | Base styles |
.ws-badge--default | Neutral variant |
.ws-badge--duration | Duration variant |
.ws-badge--category | Category variant (accepts the inline custom colour) |
.ws-badge--new | "New" label variant |
.ws-badge--count | Circular count variant |
.ws-badge--ai | AI-made variant: the AI mark on the brand tint |
.ws-badge--success / --warning / --error / --info | Status colours, emitted from the status option |
.ws-badge--sm / --md / --lg | Size modifiers |
.ws-badge--pill | Full radius |
.ws-badge__icon | Leading icon wrapper |
.ws-badge__text | Label wrapper — emitted but currently has no CSS rule. A hook for overrides, not an active style. |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_badge() plus the ws_duration(), ws_category(), and ws_status() shorthands |
includes/components/badge.php | Template — variant and status class assembly |
includes/components/components.css | Styles (.ws-badge rules) |