Detent web components
By Shane Logsdon

Feedback & status

Badge

Implemented

Rolling number animation & live status pulse

detent-badge

Port status
Catalog status
implemented
Origin
upstream
Phase
2
Wave
phase-2-feedback
Group
Feedback & status
Draft

4.5 KB gzip for the class module · inlines the shared spring

Variants

soft Active Degraded Failed
solid 12 Paid Pending Overdue
outline Beta Synced Rate limited Blocked
status dot Operational Live outage Investigating
removable chips Filter: open Assignee: you Label: urgent

Recorded states

  • focus-visible recorded state; no setter

Usage

Import

import '/components/badge/define.js';

// or register it yourself
import { DetentBadge } from '/components/badge/badge.js';

Attributes

  • dot
  • pulse
  • removable
  • tone
  • variant

Properties

  • variant
  • tone
  • dot
  • pulse
  • removable
  • onRemove

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.tsx sha256 3541dbc67cb5…

Native files

The native module and its artifacts:

  • src/components/badge/badge.ts
  • src/components/badge/styles.css
  • src/components/badge/define.ts
  • docs/components/badge.md
  • site/examples/badge.ts
  • tests/browser/badge.spec.ts
  • evidence/components/badge.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
dotdotbooleanfalsePresence semantics: present means true, absent means false.
pulsepulsebooleanfalsePresence semantics: present means true, absent means false.
removable———Recorded in the catalog as an attribute.
tonetone'neutral' | 'success' | 'warning' | 'danger''neutral'Kebab-case attribute; camelCase property.
variantvariant'soft' | 'solid' | 'outline''soft'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
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.
dotbooleanfalseProperty-only: assign from script; not accepted as an attribute.
pulsebooleanfalseProperty-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() => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-removeonRemove() => voidComposed, bubbles; fires only for a user action.

Slots

Slots
SlotNotes
defaultConsumer 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.
Previous: Avatar Stack Next: Bar Chart

Docs

Components

Project