NotificationBadge
An unread-count badge that renders nothing when the count is 0.
01 Usage
Basic
<script lang="ts">
import { NotificationBadge } from '@urbicon-ui/auth';
import { Button } from '@urbicon-ui/blocks';
let count = $state(3);
let lastClick = $state<number | null>(null);
</script>
<div class="flex items-center gap-6">
<div class="relative">
<span class="text-text-secondary text-sm">Notifications</span>
<!-- Bewusst kein `alert()`: Das Beispiel läuft auch außerhalb der
Doku-Seite (Landing-Hero), und ein Browser-Dialog blockiert dort alles
andere. Die Rückmeldung steht daneben. -->
<NotificationBadge {count} onclick={() => (lastClick = count)} />
</div>
{#if lastClick !== null}
<span class="text-text-tertiary text-sm" role="status">{lastClick} unread</span>
{/if}
<div class="flex gap-2">
<Button size="sm" variant="outlined" intent="neutral" onclick={() => count++}>+1</Button>
<Button
size="sm"
variant="outlined"
intent="neutral"
onclick={() => (count = Math.max(0, count - 1))}>-1</Button
>
<Button size="sm" variant="ghost" intent="neutral" onclick={() => (count = 0)}>Clear</Button>
</div>
</div>
02 Accessibility
Nothing is rendered at zero
The badge only exists while count > 0. At zero
it renders nothing at all, so a keyboard user tabbing past a quiet bell moves straight to
the next control instead of stopping on an empty badge.
The handler decides what it is
With an onclick the badge is a role="button" with tabindex="0", activated by Enter or Space, and it carries the pressable styling —
pointer cursor, hover and active scale. Without one it is a role="status", a polite live region: it stays out
of the tab order rather than being a focus stop on which every key is dead, and it drops
the pressable styling with the semantics, so a decorative count does not look clickable.
The role prop narrows what is announced; it does
not make the badge operable, so role="button" without a handler is a button nothing
can activate — pass an onclick for a button.
It names itself
The visible content is 3 or 99+, and the accessible name is the localized notifications.badge.unread with that same text
substituted — "Unread notifications: 3" — so the name is never a bare number. Past the cap
it says 99+ too, not the real count: a
voice-control user can only say the label they can read. Your own aria-label wins over it, as does a t override of the string. The status region exists from the first unread on — at zero
the badge renders nothing at all — so a change between two non-zero counts happens inside a
region that was already there, while the first unread arrives together with it.
03 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
count required | number | — | Number of unread notifications. Badge hidden when 0. | |
class | string | — | Extra classes on the root element. | |
onclick | () => void | — | Click handler (e.g. toggle notification center). Also what makes the badge a button and a tab stop. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ NotificationBadge: { … } }}>.
Resolves after the provider defaults and before this instance's own
slotClasses, so a project-wide look lives in one place instead of being
repeated at every usage site. | |
role | BadgeProps['role'] | — | ARIA role, passed straight to Badge — its type, so the two cannot drift.
Leave unset to take the derived one: button with an onclick, status
(a polite live region) without. It narrows what the badge is announced as;
it does not make it operable, so role="button" without an onclick is a
button nothing can activate. | |
slotClasses | Partial<Record<'root', string>> | — | Per-slot class overrides. Slots: root | |
t | PartialAuthLocale | — | Locale overrides, deep-merged over the bundle registered for the active
locale — English unless registerAuthLocale ran for it. Pass any subset,
from a single string to a whole tree. | |
unstyled | boolean | — | Strip all default styling. | |
...HTMLAttributes<HTMLElement> inherited | HTMLAttributes | — | HTML attributes (excluding: 'children' | 'class' | 'onclick' | 'role') |
04 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
NotificationBadgeProps | interface | props | 0 | — | |
PartialAuthLocale | type | helper | 1 | Consumer-facing locale input: any subset of AuthLocale. Components
accept this as their t prop and deep-merge it over the bundle registered
for the active locale — English unless registerAuthLocale ran for it — so
overriding a single string never silently blanks the rest. | |
DeepPartial | type | helper | 0 | Recursive partial: every branch and leaf becomes optional. | |
AuthLocale | interface | helper | 0 | The complete auth locale bundle. Every key is required: the bundles this
package ships (en, de) satisfy the full shape, and consumer overrides
enter as PartialAuthLocale, deep-merged by mergeAuthLocale over the
bundle registered for the active locale — English unless registerAuthLocale
ran for it — so component markup reads keys directly, without per-key
?? '…' fallback literals.
Placeholder convention: dynamic values use **single-brace** tokens
({n}, {name}, {email}) that the consuming component substitutes itself
via String.replace('{token}', value). There is deliberately **no**
{{…}} runtime interpolator in this package. The key-based translator twin
(authT/at) was removed, so {{…}} here would render verbatim. |
05 Installation
Import
import { NotificationBadge } from '@urbicon-ui/auth';