In-Degree Badge

A pill-sized circular badge that displays a single integer — most commonly the in-degree of a graph node during a Kahn-style topological sort, but generic enough for any "remaining count" annotation (frontier size, blocked dependencies, BFS layer depth). Pass value={0} with pulsing to mark a node as ready to dequeue; pass hidden to swap the digit for a ? glyph during predict-the-value interactions.

Self-contained <svg> element — the caller decides where it lives (absolutely positioned over a graph node, inline next to a label, stacked inside a larger SVG). No layout dependencies, no project-specific props.

In-degree: 0 (ready)0In-degree: 1 (idle)1In-degree: 2 (idle)2In-degree: 3 (idle)3In-degree: 2 (idle)2In-degree: 4 (idle)4
Customize
Shape
26 px
State
0
ready
Playback
700 ms

Installation

npx shadcn@latest add https://craftbits.dev/r/in-degree-badge.json

Usage

import { InDegreeBadge } from "@craft-bits/core";
 
<InDegreeBadge value={3} tone="accent" />

Mark a node as ready to dequeue:

<InDegreeBadge value={0} tone="success" pulsing />

Hide the count behind a placeholder for a predict-the-value gate:

<InDegreeBadge value={2} hidden tone="warning" />

Stack a short label above the badge:

<InDegreeBadge value={1} label="v3" size={28} />

Understanding the component

  1. Self-contained SVG. The badge renders as a viewBox-sized <svg> element, so it scales cleanly inside <foreignObject> or stacks inline next to text. The caller positions it; the component never sets top / left / transform: translate.
  2. Tone drives every color. default reads as "neutral count"; accent as "currently relevant"; success as "ready / zero remaining"; warning as "blocked"; error as "stuck / cycle detected". The ring, fill, and ink all derive from the tone variable via color-mix.
  3. Ready vs blocked. When value === 0 and the badge is not hidden, it switches into the "ready" treatment — full tone fill, full tone stroke, full tone ink. Any non-zero value falls back to a neutral chip so a dense graph stays calm.
  4. Hidden ("?") state. hidden swaps the digit for ? while keeping the underlying value in the aria-label. The chip drops to the neutral palette regardless of tone, signalling "this is a prediction, not a settled count".
  5. Pulsing ring. When value === 0 AND pulsing AND motion is allowed, a 1s breathing ring expands just outside the badge in the active tone. The ring is suppressed entirely under prefers-reduced-motion: reduce.
  6. Value transitions. Swapping value slides the new digit up from below and fades the previous digit out — built on AnimatePresence with SPRINGS.snap so the counter feels responsive on rapid algorithm ticks.
  7. Reduced motion. Enter scale, value slide, and the pulse ring all collapse to instant under prefers-reduced-motion: reduce. The text still updates; only the motion drops.

Props

PropTypeDefaultDescription
valuenumberrequiredInteger count. Negative values render as 0.
tone"default" | "accent" | "success" | "warning" | "error""default"Semantic palette.
hiddenbooleanfalseSwap the digit for ?. The original value is still announced via aria-label.
pulsingbooleanfalseBreathing ring when value === 0. Honours reduced motion.
sizenumber22Diameter (px). Floored at 12.
labelReactNodeOptional short label rendered above the badge.
ariaLabelstringOverride the announced string. Defaults to "In-degree: {value}".
transitionTransitionSPRINGS.snapEnter / value-swap transition. Reduced-motion users snap regardless.
classNamestringMerged onto the root via cn().

Accessibility

  • The outer <svg> is role="img" with a <title> derived from ariaLabel or the canonical "In-degree: {value}" string. Screen readers hear the count without parsing the SVG geometry.
  • hidden announces "In-degree: hidden" so the screen-reader experience matches the visual ? glyph — students with assistive tech see the same gate as everyone else.
  • Each badge exposes data-tone and data-state (ready / hidden / blocked) so consumer apps can hook custom styles or assistive tooling.
  • Tone is never the only signal — the value digit (or ? placeholder) renders on every state, and the ready/blocked transition shifts both the fill and the stroke so colourblind users still see the change.
  • Motion respects prefers-reduced-motion: reduce — the enter scale, value-swap slide, and pulse ring all collapse to instant.

Credits

  • Extracted from: AlgoFlashcards (src/lessons/primitives/graph/InDegreeBadge.tsx). The source was a <motion.g> fragment that expected x / y props and a raw hex colour pulled from the lesson's track. The library extract is a self-contained <svg> — the caller positions it however the parent layout wants — and the single hex is replaced by a five-tone semantic palette so the same badge covers ready / blocked / stuck / accent / neutral semantics without per-lesson colour plumbing.