Feedback & status
Badge
Rolling number animation & live status pulse
detent-badge
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 2
- Wave
- phase-2-feedback
- Group
- Feedback & status
4.5 KB gzip for the class module · inlines the shared spring
Variants
Recorded states
focus-visiblerecorded state; no setter
Usage
Import
import '/components/badge/define.js';
// or register it yourself
import { DetentBadge } from '/components/badge/badge.js';
Attributes
dotpulseremovabletonevariant
Properties
varianttonedotpulseremovableonRemove
Slots
default
Events
detent-remove— {}
Example
Runnable example: site/examples/badge.ts. The snippet below is generated from the catalogued attributes.
<detent-badge
dot
pulse
removable
tone="neutral"
variant="soft"
></detent-badge>
Property-only values
const element = document.querySelector('detent-badge');
if (element !== null) {
// onRemove: () => void
element.onRemove = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/badge.tsxsha256 3541dbc67cb5…
Native files
The native module and its artifacts:
src/components/badge/badge.tssrc/components/badge/styles.csssrc/components/badge/define.tsdocs/components/badge.mdsite/examples/badge.tstests/browser/badge.spec.tsevidence/components/badge.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
dot | dot | boolean | false | Presence semantics: present means true, absent means false. |
pulse | pulse | boolean | false | Presence semantics: present means true, absent means false. |
removable | — | — | — | Recorded in the catalog as an attribute. |
tone | tone | 'neutral' | 'success' | 'warning' | 'danger' | 'neutral' | Kebab-case attribute; camelCase property. |
variant | variant | 'soft' | 'solid' | 'outline' | 'soft' | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
variant | 'soft' | 'solid' | 'outline' | 'soft' | Property-only: assign from script; not accepted as an attribute. |
tone | 'neutral' | 'success' | 'warning' | 'danger' | 'neutral' | Property-only: assign from script; not accepted as an attribute. |
dot | boolean | false | Property-only: assign from script; not accepted as an attribute. |
pulse | boolean | false | Property-only: assign from script; not accepted as an attribute. |
removable | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
onRemove | () => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-remove | onRemove | () => void | Composed, bubbles; fires only for a user action. |
Slots
| Slot | Notes |
|---|---|
default | Consumer content; rendered through a slot, never re-created. |
CSS customisation
- React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- `onRemove` has no native callback form: the x button is gated by a `removable` boolean attribute and the callback becomes the composed `detent-remove` event.
- `detent-remove` detail is `{}` (upstream `onRemove: () => void`); the element never removes itself - the consumer owns the DOM.
- The label roll uses the upstream spring (stiffness 520 / damping 36 / mass 0.6) on the shared helper, rising from 70% below with opacity 0 to 1. Upstream also rolled the outgoing label out to -70% (AnimatePresence popLayout); the slot holds only the current text, so there is no outgoing label.
- The roll is triggered by a light-DOM MutationObserver (childList + characterData) instead of a React re-keyed span, so in-place text updates animate.
- Framer's initial/animate/layout props are framework surface and are not ported.
What it does
A compact pill for counts, statuses and removable filter chips: 3 variants × 4 tones, an optional pulsing status dot, a rolling label and an × button.
Upstream behaviour
- Numbers and text roll up with spring physics when updated
- Optional live status dot with pulsing radar ping effect
- Interactive removable chip support with animated onRemove trigger
- 3 styles (soft, solid, outline) × 4 tones (neutral, success, warning, danger)
- Motion-ready component accepting initial, animate, and layout animations directly
Differences from upstream
- React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- `onRemove` has no native callback form: the x button is gated by a `removable` boolean attribute and the callback becomes the composed `detent-remove` event.
- `detent-remove` detail is `{}` (upstream `onRemove: () => void`); the element never removes itself - the consumer owns the DOM.
- The label roll uses the upstream spring (stiffness 520 / damping 36 / mass 0.6) on the shared helper, rising from 70% below with opacity 0 to 1. Upstream also rolled the outgoing label out to -70% (AnimatePresence popLayout); the slot holds only the current text, so there is no outgoing label.
- The roll is triggered by a light-DOM MutationObserver (childList + characterData) instead of a React re-keyed span, so in-place text updates animate.
- Framer's initial/animate/layout props are framework surface and are not ported.