Flame Chart

A generic call-stack flame chart that paints each function frame as a horizontal block, stacked vertically by call depth and positioned horizontally by start time. Hover or focus any frame to surface its label, duration, depth, and start in the detail panel. Drop it into a JS-perf walkthrough, a tracing lesson, or any "where did the time go?" article.

Preview

render() call stack

Hover any frame to surface its label and timing detail.

0s100ms200ms300ms400ms500ms600ms
hover a frame to surface its detail
600ms window4 long frames
Customize
Timeline
600ms
220ms
50ms
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/flame-chart.json

Usage

import { FlameChart } from "@craft-bits/core";
 
<FlameChart
  title="render() call stack"
  windowMs={600}
  frames={[
    { id: "root", label: "render()", depth: 0, start: 0, duration: 600 },
    { id: "app", label: "<App />", depth: 1, start: 40, duration: 480 },
    { id: "diff", label: "diff()", depth: 2, start: 80, duration: 220 },
  ]}
/>

Anatomy

  • Header. Optional title (rendered with the cb-label style) and a description sub-line. Omit both for a chromeless panel.
  • Ruler. A row of tick marks across the top of the chart, auto-sized to the timeline window. Hide it with hideRuler.
  • Track. A single relative-positioned block sized by windowMs wide and (maxDepth + 1) * rowHeightPx tall. Each frame is positioned absolutely by its start, duration, and depth.
  • Block tone. Frames longer than longTaskThresholdMs paint with --cb-error; the rest paint with --cb-accent. Each block carries data-cb-verdict="long" | "short" so consumers can extend the palette.
  • Detail panel. Sits under the chart and surfaces the active frame's label, duration, depth, start, and optional detail body. Reads "hover a frame to surface its detail" when nothing is active.
  • Legend. A trailing chip summarises the long-frame count. Hide it with hideLegend.

Understanding the component

  1. Layout. Each frame renders as an absolute button inside a single track. left is derived from start / windowMs, width from duration / windowMs, and top from (maxDepth - depth) * rowHeightPx so depth 0 is the bottom row and the stack grows up. The chart infers windowMs and maxDepth from the frames when the props are omitted.
  2. Hover detail. Each frame is a focusable button. mouseenter and focus light up the detail panel; mouseleave and blur put it back. The detail panel doubles as the aria-describedby target for the chart while a frame is active.
  3. Threshold. longTaskThresholdMs defaults to 50 — the Long Tasks API definition. Override it for stricter budgets (e.g. 16 for a frame budget) or to demo the effect of shrinking the threshold.
  4. Motion. Each block fades and scaleX-springs in once on mount (spring snap). The animation short-circuits under prefers-reduced-motion.

Props

PropTypeDefaultDescription
framesFlameChartFrame[]requiredFunction-call frames to render.
titleReactNodeOptional heading above the chart.
descriptionReactNodeOptional sub-headline under the title.
windowMsnumberinferredTotal width of the chart in ms.
longTaskThresholdMsnumber50Threshold above which a frame reads as long.
rowHeightPxnumber22Height of each call-stack row in pixels.
hideLegendbooleanfalseHide the legend chip in the footer.
hideRulerbooleanfalseHide the time ruler across the top.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
classNamestringMerged onto the root via cn().

FlameChartFrame

FieldTypeDescription
idstringStable identifier.
labelReactNodeFunction or task label rendered inside the block and detail panel.
depthnumberCall-stack row, zero-indexed from the bottom.
startnumberOffset in ms from the left edge of the chart.
durationnumberLength of the frame in ms.
colorstringOptional CSS background colour override.
detailReactNodeOptional richer body for the hover detail panel.

Accessibility

  • The wrapper is a <section> with data-cb-edu="flame-chart". The track is a role="img" with an aria-label summarising the frame count, window width, and long-frame verdict.
  • Each frame is a focusable <button> with a descriptive aria-label. Keyboard users can tab through every frame and read the detail panel.
  • The detail panel is a role="status" aria-live="polite" region, and is wired as aria-describedby on the track whenever a frame is active.
  • Each block exposes data-cb-verdict="long" | "short" so consumers can extend the palette without monkey-patching CSS.
  • Animations are limited to the block enter and short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/perf-javascript/ui/FlameChart.tsx). The original was wired to a project-level js-perf-simulator engine, hard-coded a "Main" / "Worker" lane pair, and rendered TTI / first-paint / click-event markers from a project script id. This rewrite drops every project-specific prop and the simulator chrome — consumers pass their own frames array with depth, so the chart visualises any call stack, not just the JS-perf lesson timeline.