Components · Data Display
Badge
A small count or status marker — notification counts, unread dots, a standalone status label.
Examples
Status colour
Count
Ribbon placement
Tone chip
Plan chip
Rank chip
Count · over a wrapped child · capped as N+ · custom colour
Dot · a bare marker in each status colour
Status · a standalone semantic dot + label
Tone · filled/tinted label chips (each bound to an --aha token)
Plan · preset chips (solid brand fill · cycle · crown on pro/enterprise)
Rank · leaderboard medal chips · and Session
Clickable chip = a LINK (<a href>), not a button · hover-animates
Ribbon · a corner banner wrapping a card · start / end placement
Quiz template
Fresh from the gallery
Pro plan
50% off this week
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-badge> is the SAME shared custom element React and Vue consume — here in its
native form. Theming comes only from the --aha-* tokens in tokens.css. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/icons.js'; // registers <aha-icon>
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-badge.js'; // registers <aha-badge>
</script>
<div style="display:flex; gap:24px; align-items:center">
<!-- count over a wrapped child, capped as N+ -->
<aha-badge count="128" overflow-count="99">
<aha-icon name="system-bell" size="24"></aha-icon>
</aha-badge>
<!-- a bare status dot -->
<aha-badge dot status="success"></aha-badge>
<!-- a standalone status: a semantic dot + a label -->
<aha-badge status="processing" text="Generating"></aha-badge>
</div>
<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:24px">
<!-- TONE — a filled/tinted label chip (danger·success·positive·essential·pro·branding·primary-alt) -->
<aha-badge tone="essential">Essential</aha-badge>
<aha-badge tone="pro">Pro</aha-badge>
<aha-badge tone="branding">New</aha-badge>
<!-- PLAN preset chips — pro/enterprise lead with a crown; `cycle` appends the billing cycle -->
<aha-badge plan="free"></aha-badge>
<aha-badge plan="pro" cycle="yearly"></aha-badge>
<!-- RANK (leaderboard) + SESSION -->
<aha-badge rank="1"></aha-badge>
<aha-badge session="3"></aha-badge>
<!-- CLICKABLE = a LINK (<a href>), never a <button>. Navigates to upgrade; animates on hover. -->
<aha-badge plan="pro" href="/billing/upgrade"></aha-badge>
</div>
<!-- ribbon — a corner banner wrapping a card (placement end = top-right, default) -->
<aha-badge ribbon text="New" style="display:inline-block; margin-top:24px">
<div style="width:200px; padding:20px; border:1px solid var(--aha-border,#E3E3E3); border-radius:var(--aha-radius-default,8px); background:var(--aha-bg-container,#fff)">
Quiz template
</div>
</aha-badge>
import '@ahaslides-product/design/aha-badge'; // registers <aha-badge>
import '@ahaslides-product/design/icons'; // registers <aha-icon>
// Attributes only — no events — so React 18/19 can render the element directly.
// Boolean attrs (dot/showZero) are the attribute PRESENCE, so spread them conditionally.
function AhaBadge({ count, overflowCount, dot, status, text, color, showZero, ribbon, placement,
tone, plan, cycle, rank, session, href, children }) {
return (
<aha-badge
count={count}
overflow-count={overflowCount}
status={status}
text={text}
color={color}
placement={placement}
tone={tone}
plan={plan}
cycle={cycle}
rank={rank}
session={session}
href={href}
{...(dot ? { dot: '' } : {})}
{...(showZero ? { 'show-zero': '' } : {})}
{...(ribbon ? { ribbon: '' } : {})}
>
{children}
</aha-badge>
);
}
// count over a wrapped child
<AhaBadge count={unread} overflowCount={99}>
<BellIcon />
</AhaBadge>
// a standalone status badge — a semantic dot + label
<AhaBadge status="processing" text="Generating" />
// a corner ribbon banner wrapping a card (placement start/end)
<AhaBadge ribbon text="New">
<TemplateCard />
</AhaBadge>
// a tinted TONE label chip
<AhaBadge tone="essential">Essential</AhaBadge>
// a PLAN preset chip (pro/enterprise show a crown; cycle appends the billing cycle)
<AhaBadge plan="pro" cycle="yearly" />
// a leaderboard RANK chip + a SESSION chip
<AhaBadge rank={1} />
<AhaBadge session={3} />
// CLICKABLE = a LINK (<a href>), NOT a button — navigates to upgrade, animates on hover
<AhaBadge plan="pro" href="/billing/upgrade" />
// main.ts — register the elements + mark aha-* as custom elements
import '@ahaslides-product/design/aha-badge';
import '@ahaslides-product/design/icons';
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const unread = ref(128);
</script>
<template>
<!-- count over a wrapped child, capped as N+ -->
<aha-badge :count="unread" overflow-count="99">
<aha-icon name="system-bell" size="24" />
</aha-badge>
<!-- a standalone status badge — a semantic dot + label -->
<aha-badge status="processing" text="Generating" />
<!-- a corner ribbon banner wrapping a card (placement start/end) -->
<aha-badge ribbon text="New">
<div class="template-card">Quiz template</div>
</aha-badge>
<!-- a tinted TONE label chip -->
<aha-badge tone="essential">Essential</aha-badge>
<!-- a PLAN preset chip (pro/enterprise show a crown; cycle appends the billing cycle) -->
<aha-badge plan="pro" cycle="yearly" />
<!-- a leaderboard RANK chip + a SESSION chip -->
<aha-badge rank="1" />
<aha-badge session="3" />
<!-- CLICKABLE = a LINK (<a href>), NOT a button — navigates to upgrade, animates on hover -->
<aha-badge plan="pro" href="/billing/upgrade" />
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
count | number | 0 | The number shown in the bubble; 0 hides it unless showZero |
overflowCount | number | 99 | Cap; above it renders as N+ (alias: max) |
showZero | boolean | false | Keep the bubble visible when count is 0 |
dot | boolean | false | Render a bare marker, no number |
status | success | processing | error | warning | default | primary | error | Semantic colour; with `text` (no count/dot) renders a standalone dot + label |
text | string | — | Label for a standalone status badge |
color | CSS colour | — | Custom colour overriding the semantic default |
ribbon | boolean | false | Wrap the slotted child (a card) with a corner ribbon banner; `text` is the label, colour from color/status (default primary) |
placement | start | end | end | Ribbon corner: end = top-right (default), start = top-left |
tone | danger | success | positive | essential | pro | branding | primary-alt | — | Filled/tinted LABEL chip; each tone binds bg + fg to an --aha-* token |
plan | free | edu | essential | pro | enterprise | — | Plan preset chip (solid brand fill); pro/enterprise lead with a crown glyph |
cycle | monthly | yearly | — | Appended to a plan chip's default label (e.g. "Pro · Yearly") |
rank | 1 | 2 | 3 | 4 | — | Leaderboard rank chip; medal-tinted (gold/silver/bronze) with a leading system-Medal glyph |
session | string | — | Renders a "Session N" chip; the value is N (slotted content / `text` overrides the label) |
href | URL | — | Makes a tone/plan/rank/session chip a clickable LINK — renders <a class="chip" href> (native focusable, animates on hover), never a <button>. Omit for a static <span> chip |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-badge'; // registers <aha-badge>
Agent feed for this component (absolute, fetchable anywhere): badge.agent.json · badge.md · badge.llms.txt
When to use
When to use
- Badge count/dot — a count or presence marker attached to another element (a bell, a tab, an avatar)
- Badge status — a standalone semantic state label (Published, Generating, Failed) in a list or header
- Tag — a standalone label/keyword chip with text but no status semantics
A count/dot marks another element — wrap the element it belongs to so the pill anchors to its top-right. Keep counts meaningful; cap large numbers with overflowCount so the bubble stays small. Reserve the standalone status form for a semantic lifecycle state, and let processing pulse rather than spin a separate loader.
Surfaces
editor dashboard settings audience
Spec
Count bubble height 18 · min-width 18 · radius pill · padding 0 6 · scales in · On a child floats to the child's top-right, 1px container-colour ring · Dot 8×8, no label · Status standalone 6px dot + 14px label; processing pulses · Text Plus Jakarta SemiBold 600, 11/18, #FFFFFF · Default colour count/dot → error #F5222D; status → its semantic token; ribbon → primary #6A1EBB · Overflow / zero count > overflowCount → N+; 0 hides unless showZero · Ribbon corner banner over a wrapped card; 22px tall, radius 4, triangular fold; placement start/end · Tone chip filled/tinted pill (radius pill, 22px); danger·success·positive·essential·pro·branding·primary-alt, each bound to an --aha token · Plan chip preset (free·edu·essential·pro·enterprise); solid brand fill, cycle monthly/yearly; pro/enterprise lead with a crown · Rank chip leaderboard 1st–4th, medal-tinted (gold/silver/bronze), leading system-Medal glyph · Session chip "Session N" brand-accent pill; label from `session` / slotted text · Clickable = link with `href` renders <a class="chip"> (native focusable link, animates on hover) — never a <button>; no href = plain <span>