Skip to main content

ns-ui

Glaze Crawl Heal

A live status badge drawn as a glaze surface mid-firing: gas bubbles burst through the molten glaze and the melt either flows back over the crater (heals) or, when surface tension has failed, pulls away into a ragged bare patch that never closes (crawling). Three real ceramics behaviours standing in for operational, degraded and down.

Usage

registry/core/glaze-crawl-heal/demo.tsx
"use client";

import { useEffect, useState } from "react";
import { GlazeCrawlHeal, type GlazeStatus } from "./component";

// a realistic system-status card: one live indicator riding out an incident
// and back, plus fixed reference rows so all three kiln states sit on
// screen at once for comparison. No pointer or keyboard in the loop — this
// is an ambient control, not an interactive one.
const RUN: { status: GlazeStatus; ms: number }[] = [
  { status: "healthy", ms: 4200 },
  { status: "degraded", ms: 4600 },
  { status: "down", ms: 4400 },
];

const ROWS: { status: GlazeStatus; service: string }[] = [
  { status: "healthy", service: "API" },
  { status: "degraded", service: "Search index" },
  { status: "down", service: "Webhooks" },
];

export default function GlazeCrawlHealDemo() {
  const [step, setStep] = useState(0);
  const active = RUN[step % RUN.length]!;

  useEffect(() => {
    const t = window.setTimeout(() => setStep((s) => s + 1), active.ms);
    return () => window.clearTimeout(t);
  }, [step, active.ms]);

  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-12 px-6 py-16">
      <p className="font-mono text-xs uppercase tracking-[0.2em] text-ns-muted">
        ns-ui / glaze-crawl-heal
      </p>

      <div className="flex flex-col items-center gap-10 sm:flex-row sm:items-stretch">
        {/* showpiece — large enough that bubble/crater/patch behaviour clearly reads */}
        <div className="flex flex-col items-center justify-center gap-5 rounded-xl border border-border bg-surface px-10 py-10">
          <GlazeCrawlHeal status={active.status} size={72} showLabel={false} />
          <GlazeCrawlHeal status={active.status} size={16} />
        </div>

        {/* status page row — the realistic inline usage, badge scale */}
        <div className="flex w-[300px] flex-col gap-4 rounded-xl border border-border bg-surface p-5">
          <div className="flex items-center justify-between">
            <span className="text-sm text-foreground">Platform</span>
            <GlazeCrawlHeal status={active.status} size={14} />
          </div>
          <div className="h-px w-full bg-border" />
          <ul className="flex flex-col gap-3">
            {ROWS.map((row) => (
              <li key={row.service} className="flex items-center justify-between">
                <span className="text-xs text-ns-muted">{row.service}</span>
                <GlazeCrawlHeal status={row.status} size={14} />
              </li>
            ))}
          </ul>
        </div>
      </div>

      <p className="max-w-md text-center font-mono text-[10px] leading-relaxed text-ns-muted">
        bubbles burst and heal — operational closes in under a second,
        degraded lingers, down crawls into bare patches that never close
      </p>
    </div>
  );
}

Component

registry/core/glaze-crawl-heal/component.tsx
"use client";

import { useLayoutEffect, useRef } from "react";

// ---------------------------------------------------------------------------
// GlazeCrawlHeal — a live status badge drawn as a glaze surface mid-firing,
// not three arbitrary colour chips. In a kiln the glaze goes molten and gas
// bubbles rise through the melt and burst at the surface, leaving craters;
// whether the melt's viscosity and surface tension let it FLOW BACK over
// that crater (heals smooth) or not (crawling — the glaze pulls away from
// the body into a bare patch that never closes) is a real, named ceramics
// defect, not an invented metaphor. That gives the badge one physical state
// axis instead of a colour code: healthy = bubbles burst and heal in well
// under a second; degraded = the melt is thicker, craters linger for
// seconds before closing; down = surface tension has failed outright —
// bubbles still burst (gas still escapes) but the melt never flows back, so
// ragged, beaded-rim bare patches accumulate and persist. Unlike craze-rule
// (a static fracture-crack divider that draws in once and idles), this has
// no crack geometry at all — it is continuous bubble nucleation, bursting
// and healing/failing-to-heal, forever, because the glaze stays molten for
// as long as the system is up. The canvas is pure decoration on top of a
// real, always-present text status (STATUS_LABEL) inside a role="status"
// aria-live region — colour never carries the message alone. Ink is read
// once via getComputedStyle(canvas).color (the canvas carries text-
// foreground) in useLayoutEffect, before first paint, and re-read on a
// documentElement class MutationObserver; craters/patches are punched with
// globalCompositeOperation="destination-out" rather than painted in a
// second token, so a "hole" always reveals whatever sits behind the badge —
// dark-on-light in light theme, light-on-dark in dark, for free. Zero deps,
// one DPR-capped canvas, one rAF loop paused on prefers-reduced-motion,
// tab-hidden and unmount.
// ---------------------------------------------------------------------------

export type GlazeStatus = "healthy" | "degraded" | "down";

export interface GlazeCrawlHealProps {
  /** the kiln state to render. default "healthy". */
  status?: GlazeStatus;
  /**
   * accessible text announced via the component's own role="status" region
   * on every change. Defaults to the plain status name ("Operational").
   */
  label?: string;
  /** render `label` as a visible text node beside the badge (still announced either way). default true — a status control's text is not optional. */
  showLabel?: boolean;
  /** badge diameter in px. Reads down to ~14; below that the mechanic itself stops resolving and only the text still carries the state — see component header. default 16. */
  size?: number;
  /** extra classes merged onto the rendered root element */
  className?: string;
}

const STATUS_LABEL: Record<GlazeStatus, string> = {
  healthy: "Operational",
  degraded: "Degraded",
  down: "Down",
};

const TAU = Math.PI * 2;
const JAG_POINTS = 11; // ragged-edge vertex count for a crawled (bare) patch

// Per-status kiln parameters. healMs is the viscosity/surface-tension read:
// short = fluid enough to close fast, long = it only just manages to close.
// A "down" patch never gets a healMs of its own — it doesn't heal — until
// the badge recovers to a status that has one (see the status-change effect
// below), at which point the melt finally flows back over it.
const BUBBLE_CONFIG: Record<
  "healthy" | "degraded",
  {
    spawnMs: [number, number];
    openMs: number;
    healMs: [number, number];
    maxRFrac: number;
    maxConcurrent: number;
  }
> = {
  healthy: { spawnMs: [650, 1500], openMs: 150, healMs: [350, 550], maxRFrac: 0.3, maxConcurrent: 1 },
  degraded: { spawnMs: [1100, 2200], openMs: 220, healMs: [1700, 3100], maxRFrac: 0.42, maxConcurrent: 2 },
};
const PATCH_CONFIG = {
  spawnMs: [900, 1800] as [number, number],
  creepTauMs: 1300,
  maxRFrac: 0.52,
  maxConcurrent: 3,
};

function easeOutCubic(t: number) {
  const p = 1 - t;
  return 1 - p * p * p;
}
function easeInOutCubic(t: number) {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

/** mulberry32 — small, fast, deterministic given a seed */
function mulberry32(seed: number) {
  let s = seed | 0;
  return () => {
    s = (s + 0x6d2b79f5) | 0;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/** per-vertex radius multipliers (0.72..1.06) for a crawled patch's ragged rim */
function makeJag(seed: number): number[] {
  const rng = mulberry32(seed);
  const out: number[] = [];
  for (let i = 0; i < JAG_POINTS; i++) out.push(0.72 + rng() * 0.34);
  return out;
}

// fixed seeds -> deterministic, byte-stable jag shapes for the
// prefers-reduced-motion freeze frame (no Math.random there, ever).
const DOWN_JAG_A = makeJag(101);
const DOWN_JAG_B = makeJag(202);

type Feature = {
  kind: "bubble" | "patch";
  x: number;
  y: number; // offset from disc centre, CSS px
  maxR: number; // CSS px
  bornAt: number;
  openMs: number;
  healMs: number; // set once healing begins; Infinity/unused until then
  healingSince: number | null;
  healStartR: number;
  creepTau: number;
  wobblePhase: number;
  jag: number[] | null; // null = perfect circle (bubble); set = ragged patch
};

type DrawSpec = {
  x: number;
  y: number;
  r: number;
  jag: number[] | null;
  rimAlpha: number;
  flash: number;
};

function drawFeature(
  ctx: CanvasRenderingContext2D,
  ink: string,
  cx: number,
  cy: number,
  R0: number,
  f: DrawSpec,
) {
  if (f.r <= 0.3) return;
  ctx.save();
  ctx.translate(cx + f.x, cy + f.y);

  ctx.globalCompositeOperation = "destination-out";
  ctx.beginPath();
  if (f.jag) {
    for (let i = 0; i < JAG_POINTS; i++) {
      const a = (i / JAG_POINTS) * TAU;
      const rad = f.r * f.jag[i]!;
      const px = Math.cos(a) * rad;
      const py = Math.sin(a) * rad;
      if (i === 0) ctx.moveTo(px, py);
      else ctx.lineTo(px, py);
    }
    ctx.closePath();
  } else {
    ctx.arc(0, 0, f.r, 0, TAU);
  }
  ctx.fill();

  ctx.globalCompositeOperation = "source-over";
  if (f.jag) {
    // beaded rim: glaze that pulled away balls up at the receding edge
    // instead of flowing back — the visible signature of crawling.
    ctx.fillStyle = ink;
    for (let i = 0; i < JAG_POINTS; i++) {
      const a = (i / JAG_POINTS) * TAU;
      const rad = f.r * f.jag[i]!;
      const px = Math.cos(a) * rad;
      const py = Math.sin(a) * rad;
      ctx.globalAlpha = 0.5 + 0.35 * ((i * 5) % 4) / 4;
      ctx.beginPath();
      ctx.arc(px, py, Math.max(0.55, R0 * 0.045), 0, TAU);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  } else if (f.rimAlpha > 0) {
    // healing crater's raised lip
    ctx.strokeStyle = ink;
    ctx.globalAlpha = f.rimAlpha;
    ctx.lineWidth = Math.max(0.6, R0 * 0.05);
    ctx.beginPath();
    ctx.arc(0, 0, f.r + R0 * 0.02, 0, TAU);
    ctx.stroke();
    ctx.globalAlpha = 1;
  }
  if (f.flash > 0) {
    ctx.strokeStyle = ink;
    ctx.globalAlpha = f.flash;
    ctx.lineWidth = Math.max(0.8, R0 * 0.09);
    ctx.beginPath();
    ctx.arc(0, 0, f.r * 1.02, 0, TAU);
    ctx.stroke();
    ctx.globalAlpha = 1;
  }
  ctx.restore();
}

function renderFrame(
  ctx: CanvasRenderingContext2D,
  ink: string,
  w: number,
  h: number,
  cx: number,
  cy: number,
  R0: number,
  sheenAngle: number,
  specs: DrawSpec[],
) {
  ctx.clearRect(0, 0, w, h);
  ctx.globalCompositeOperation = "source-over";
  ctx.fillStyle = ink;
  ctx.globalAlpha = 1;
  ctx.beginPath();
  ctx.arc(cx, cy, R0, 0, TAU);
  ctx.fill();

  // a slow-drifting specular dim patch — the melt still reads as liquid in
  // the gaps between bubble events, never a flat dead disc.
  ctx.globalCompositeOperation = "destination-out";
  const sx = cx + Math.cos(sheenAngle) * R0 * 0.5;
  const sy = cy + Math.sin(sheenAngle) * R0 * 0.5;
  ctx.globalAlpha = 0.08;
  ctx.beginPath();
  ctx.ellipse(sx, sy, R0 * 0.42, R0 * 0.3, sheenAngle, 0, TAU);
  ctx.fill();
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = "source-over";

  for (const f of specs) drawFeature(ctx, ink, cx, cy, R0, f);
}

/** deterministic freeze frame per status — craters/patches at varied stages of healing, never mid-t0 */
function staticSpecs(status: GlazeStatus, R0: number): DrawSpec[] {
  if (status === "healthy") {
    const maxR = R0 * BUBBLE_CONFIG.healthy.maxRFrac;
    return [
      { x: -R0 * 0.3, y: -R0 * 0.12, r: maxR * 0.55, jag: null, rimAlpha: 0.35, flash: 0 },
      { x: R0 * 0.34, y: R0 * 0.28, r: maxR * 0.15, jag: null, rimAlpha: 0.45, flash: 0 },
    ];
  }
  if (status === "degraded") {
    const maxR = R0 * BUBBLE_CONFIG.degraded.maxRFrac;
    return [
      { x: -R0 * 0.3, y: -R0 * 0.22, r: maxR * 0.92, jag: null, rimAlpha: 0.5, flash: 0 },
      { x: R0 * 0.28, y: R0 * 0.1, r: maxR * 0.55, jag: null, rimAlpha: 0.4, flash: 0 },
      { x: 0, y: R0 * 0.36, r: maxR * 0.2, jag: null, rimAlpha: 0.45, flash: 0 },
    ];
  }
  const maxR = R0 * PATCH_CONFIG.maxRFrac;
  return [
    { x: -R0 * 0.24, y: -R0 * 0.18, r: maxR * 0.9, jag: DOWN_JAG_A, rimAlpha: 0, flash: 0 },
    { x: R0 * 0.3, y: R0 * 0.26, r: maxR * 0.4, jag: DOWN_JAG_B, rimAlpha: 0, flash: 0 },
  ];
}

export function GlazeCrawlHeal({
  status = "healthy",
  label,
  showLabel = true,
  size = 16,
  className = "",
}: GlazeCrawlHealProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const engineRef = useRef<{ setStatus: (s: GlazeStatus) => void } | null>(null);

  // -- token derivation happens before first paint, so nothing ever paints
  // with a default/unset ink colour --
  useLayoutEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    let ink = "";
    let w = 0;
    let h = 0;
    let cx = 0;
    let cy = 0;
    let R0 = 0;
    let raf = 0;
    let nextSpawnAt = 0;
    let statusCur: GlazeStatus = status;
    const features: Feature[] = [];

    const readInk = () => {
      ink = getComputedStyle(canvas).color;
    };

    const resize = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      w = size;
      h = size;
      canvas.width = Math.max(1, Math.round(w * dpr));
      canvas.height = Math.max(1, Math.round(h * dpr));
      canvas.style.width = `${w}px`;
      canvas.style.height = `${h}px`;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      cx = w / 2;
      cy = h / 2;
      R0 = (Math.min(w, h) / 2) * 0.86;
    };

    const drawStatic = () => {
      renderFrame(ctx, ink, w, h, cx, cy, R0, -0.8, staticSpecs(statusCur, R0));
    };

    const maxRFor = (kind: "healthy" | "degraded" | "down") =>
      R0 * (kind === "down" ? PATCH_CONFIG.maxRFrac : BUBBLE_CONFIG[kind].maxRFrac);

    let featureSeed = 1;

    const maybeSpawn = (now: number) => {
      if (now < nextSpawnAt) return;
      if (statusCur === "down") {
        if (features.length < PATCH_CONFIG.maxConcurrent) {
          const margin = Math.max(0, R0 * 0.9 - maxRFor("down"));
          const ang = Math.random() * TAU;
          const dist = Math.random() * margin;
          features.push({
            kind: "patch",
            x: Math.cos(ang) * dist,
            y: Math.sin(ang) * dist,
            maxR: maxRFor("down") * (0.6 + Math.random() * 0.4),
            bornAt: now,
            openMs: 0,
            healMs: 0,
            healingSince: null,
            healStartR: 0,
            creepTau: PATCH_CONFIG.creepTauMs * (0.8 + Math.random() * 0.5),
            wobblePhase: Math.random() * TAU,
            jag: makeJag(featureSeed++),
          });
        }
        const [a, b] = PATCH_CONFIG.spawnMs;
        nextSpawnAt = now + a + Math.random() * (b - a);
        return;
      }
      const cfg = BUBBLE_CONFIG[statusCur];
      if (features.length < cfg.maxConcurrent) {
        const margin = Math.max(0, R0 * 0.9 - maxRFor(statusCur));
        const ang = Math.random() * TAU;
        const dist = Math.random() * margin;
        const [hMin, hMax] = cfg.healMs;
        features.push({
          kind: "bubble",
          x: Math.cos(ang) * dist,
          y: Math.sin(ang) * dist,
          maxR: R0 * cfg.maxRFrac * (0.7 + Math.random() * 0.5),
          bornAt: now,
          openMs: cfg.openMs,
          healMs: hMin + Math.random() * (hMax - hMin),
          healingSince: null,
          healStartR: 0,
          creepTau: 0,
          wobblePhase: 0,
          jag: null,
        });
      }
      const [a, b] = cfg.spawnMs;
      nextSpawnAt = now + a + Math.random() * (b - a);
    };

    const tick = (now: number) => {
      raf = requestAnimationFrame(tick);
      if (document.hidden) return;

      maybeSpawn(now);

      const specs: DrawSpec[] = [];
      for (let i = features.length - 1; i >= 0; i--) {
        const f = features[i]!;
        const age = now - f.bornAt;
        let r = 0;
        let rimAlpha = 0;
        let flash = 0;
        let dead = false;

        if (f.kind === "bubble") {
          if (f.healingSince == null) {
            if (age < f.openMs) {
              r = f.maxR * easeOutCubic(f.openMs > 0 ? age / f.openMs : 1);
            } else {
              f.healingSince = f.bornAt + f.openMs;
            }
          }
          if (f.healingSince != null) {
            const hAge = now - f.healingSince;
            if (hAge >= f.healMs) {
              dead = true;
            } else {
              r = f.maxR * (1 - easeInOutCubic(hAge / f.healMs));
              rimAlpha = 0.15 + 0.35 * (1 - hAge / f.healMs);
            }
          }
          const peak = f.bornAt + f.openMs;
          const df = Math.abs(now - peak);
          if (df < 120) flash = 1 - df / 120;
        } else {
          // patch — burst once (gas still escapes) then either creeps
          // (still failing to close) or, post-recovery, heals like a bubble
          if (f.healingSince == null) {
            r = f.maxR * (1 - Math.exp(-age / f.creepTau));
            r *= 1 + 0.03 * Math.sin(age * 0.0025 + f.wobblePhase);
            if (age < 130) flash = 1 - age / 130;
          } else {
            const hAge = now - f.healingSince;
            if (hAge >= f.healMs) {
              dead = true;
            } else {
              r = f.healStartR * (1 - easeInOutCubic(hAge / f.healMs));
            }
          }
        }

        if (dead) {
          features.splice(i, 1);
          continue;
        }
        specs.push({ x: f.x, y: f.y, r, jag: f.jag, rimAlpha, flash });
      }

      const sheenAngle = now * 0.00015;
      renderFrame(ctx, ink, w, h, cx, cy, R0, sheenAngle, specs);
    };

    const start = () => {
      if (reduced || raf) return;
      raf = requestAnimationFrame(tick);
    };
    const stop = () => {
      cancelAnimationFrame(raf);
      raf = 0;
    };

    engineRef.current = {
      setStatus: (next) => {
        const prev = statusCur;
        statusCur = next;
        if (prev === "down" && next !== "down") {
          // conditions recovered — the melt can finally flow back over
          // whatever crawled while it was down.
          const now = performance.now();
          const cfg = BUBBLE_CONFIG[next];
          const [hMin, hMax] = cfg.healMs;
          for (const f of features) {
            if (f.kind === "patch" && f.healingSince == null) {
              const age = now - f.bornAt;
              const r =
                f.maxR *
                (1 - Math.exp(-age / f.creepTau)) *
                (1 + 0.03 * Math.sin(age * 0.0025 + f.wobblePhase));
              f.healStartR = r;
              f.healingSince = now;
              f.healMs = hMin + Math.random() * (hMax - hMin);
            }
          }
        }
        if (reduced) drawStatic();
        else start();
      },
    };

    const onVis = () => {
      if (!document.hidden) start();
    };
    document.addEventListener("visibilitychange", onVis);

    const mo = new MutationObserver(() => {
      readInk();
      if (reduced) drawStatic();
    });
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });

    readInk();
    resize();
    if (reduced) drawStatic();
    else start();

    return () => {
      engineRef.current = null;
      stop();
      mo.disconnect();
      document.removeEventListener("visibilitychange", onVis);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [size]);

  useLayoutEffect(() => {
    engineRef.current?.setStatus(status);
  }, [status]);

  const text = label ?? STATUS_LABEL[status];

  return (
    <span
      role="status"
      aria-live="polite"
      data-glaze-status={status}
      className={`inline-flex shrink-0 items-center gap-2 ${className}`}
    >
      <span
        aria-hidden
        className="inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border"
        style={{ width: size, height: size }}
      >
        <canvas ref={canvasRef} className="block text-foreground" />
      </span>
      {showLabel ? (
        <span className="font-mono text-xs text-foreground">{text}</span>
      ) : (
        <span className="sr-only">{text}</span>
      )}
    </span>
  );
}

The usage example imports ./component because it sits next to the component file in this repo. Installed, that file lands at components/ui/glaze-crawl-heal.tsx, so point the import there.

Use when Pick glaze-crawl-heal for a small, always-on system status dot/badge whose three states must read from behaviour and value alone, with no hue — its motion is continuous bubble burst-and-heal, never a static glyph. status-sphere-dots and status-glyph-cadence are the better pick for a single in-progress process (an assistant's own thinking/searching/done cadence) rather than a persisted up/degraded/down system state; craze-rule is unrelated — a one-shot section divider, not a status control.

Install

npx shadcn add https://design.helpmarq.com/r/glaze-crawl-heal.json

Ask AI

Point an assistant at this component's docs (llms-full.txt) with one click.

Claude, ChatGPT, Grok, and Perplexity open with the prompt already in. Gemini copies it to your clipboard first. Paste it in once the chat opens.

Build spec

Build <GlazeCrawlHeal status? label? showLabel? size? className?> as a live status badge sourced from real glaze-firing behaviour, not an invented colour code. GLAZE PHYSICS: in a kiln the glaze goes molten and gas bubbles rise through the melt and burst at the surface, leaving craters; whether the melt's viscosity and surface tension let it flow back over the crater (heal) or not (crawling — the glaze pulls away from the clay body into a bare patch that never closes) is a documented ceramics defect, and it is the component's entire state axis: healthy = bubbles burst and heal in well under a second (short healMs, thin/fluid melt), degraded = the melt is thicker so craters linger for one to three seconds before closing (long healMs, same mechanic, different viscosity), down = surface tension has failed outright — bubbles still burst (gas still escapes the melt) but the melt never flows back, so ragged, beaded-rim bare patches accumulate and persist until conditions recover. RENDERING: one DPR-capped <canvas> circle inset inside a border-border ring. Every frame redraws a solid disc filled with the canvas's own colour (getComputedStyle(canvas).color, the canvas carries text-foreground), then punches each live bubble/patch into that disc via globalCompositeOperation='destination-out' at its current radius — a perfect circle for a bubble, an 11-vertex ragged polygon (per-vertex radius 0.72-1.06x nominal, from a seeded mulberry32 walk) for a crawled patch. Punching with destination-out rather than painting a second token means a 'hole' always reveals whatever is genuinely behind the badge, so molten ink is dark-on-light in light theme and light-on-dark in dark theme for free, with no theme branch in the draw code. A healing crater gets a thin foreground rim stroke (its raised lip) that fades as it closes; a crawled patch gets small foreground beads at its ragged vertices instead (glaze balled up at the receding edge rather than flowing back) and never gets a rim, because it isn't healing. A slow-drifting, low-alpha destination-out ellipse (0.08 alpha, ~0.00015 rad/ms) sweeps across the disc as a specular sheen so the surface still reads as liquid in the seconds between bubble events. LIFECYCLE: a bubble opens 0->maxR over openMs (ease-out-cubic, the pop) then closes maxR->0 over a per-status randomised healMs (ease-in-out-cubic) and is removed; a momentary full-strength rim flash brackets the peak, selling the burst. A down-state patch bursts open the same way but then creeps 0->maxR on an exponential approach (r = maxR*(1-e^(-age/tau)), tau ~1.3s) with a small ambient wobble, asymptotically approaching but never reaching maxR — it simply never gets a healMs, so it never closes, which is what makes 'never heals' true rather than merely slow. Concurrency and nucleation cadence differ per status (healthy: 1 bubble, 650-1500ms between spawns; degraded: up to 2, 1100-2200ms; down: up to 3 patches, 900-1800ms) so the badge is genuinely alive at rest at every status, with no settled end state — screenshots taken a mount, +2.5s and +5s all differ. RECOVERY: when status transitions away from 'down', every live patch is handed a real healMs drawn from the new status's own healthy/degraded range and its current creeping radius becomes its heal-start radius, so it finally flows back closed on the same ease-in-out curve a bubble uses — the melt genuinely repairs itself once conditions are fixed, it does not just vanish. A11Y: the root is a <span role='status' aria-live='polite'> so every status change is announced by assistive tech; the canvas is aria-hidden and is decoration only — the accessible name and the announced text are one real string (STATUS_LABEL: 'Operational' / 'Degraded' / 'Down', or a caller-supplied label), rendered as a visible text node by default (showLabel) or as a visually-hidden span when showLabel is false, so a colour-blind or screen-reader user always gets the same information a sighted user gets from the canvas. No throttling logic is needed beyond that — status is caller-driven, discrete state, not a per-frame value. TOKENS: ink is read once via getComputedStyle(canvas).color in useLayoutEffect, before first paint, and re-read on a documentElement class MutationObserver; every stroke/fill on the canvas is that one ink value composited with alpha, so nothing is a second colour — --ns-accent never appears, because this is ambient state, not interaction chrome, and the badge's own outline ring is the one legitimate --border use. SCALE: default size is 16px, a realistic inline status-dot scale, and the mechanic (a bursting/healing/crawling disc) is proportioned to read down to about 14px; below roughly 10-12px the ragged-patch texture and rim detail stop resolving to the eye and the text label becomes the only reliable channel — stated plainly rather than hidden, which is exactly why the text is never optional to the accessibility tree. REDUCED MOTION: window.matchMedia('(prefers-reduced-motion: reduce)') freezes the canvas on one deterministic, non-t0 still per status built from fixed offsets/radii (and, for 'down', two fixed-seed ragged shapes) rather than any live simulation state — healthy shows one crater mid-heal and one nearly closed, degraded shows three craters at clearly different open/closing stages, down shows one large stalled patch and one smaller forming one — so the mechanic is legible in a single frame and the frame is byte-stable across repeated captures; status text and the aria-live region stay fully live regardless of the motion preference. Props: status ('healthy' | 'degraded' | 'down', default 'healthy'), label (accessible override), showLabel (default true), size (px, default 16), className. Zero dependencies, no canvas resize observer needed since size is a controlled prop rather than a fluid container.

Props

PropTypeDefaultDescription
status?GlazeStatus"healthy"the kiln state to render. default "healthy".
label?string—accessible text announced via the component's own role="status" region on every change. Defaults to the plain status name ("Operational").
showLabel?booleantruerender `label` as a visible text node beside the badge (still announced either way). default true — a status control's text is not optional.
size?number16badge diameter in px. Reads down to ~14; below that the mechanic itself stops resolving and only the text still carries the state — see component header. default 16.
className?string—extra classes merged onto the rendered root element