Skip to content
Viewership Inquire
UI Components

Flowchart.

Workflow trigger and condition cards on a dotted canvas with draggable steps.

DataComponentNo dependencies Beautiful UI, MIT

.md
Arrow keys move this card.
Trigger
If / Else
If request is
and request is

Using Claude Code? Give it the agent prompt and skip these steps.

  1. Import the foundation once

    foundation.css holds the tokens every component reads.

    Layout.astro
    ---
    // src/layouts/Layout.astro
    import "../styles/foundation.css";
    ---
  2. Copy the 2 files

    Paste into src/components/viewership/flowchart/

  3. Use it

    Page.astro
    ---
    import Flowchart from "../components/viewership/flowchart/Flowchart.astro";
    ---
    
    <Flowchart />

Props 4

PropTypeDefaultDescription
steps FlowStep[] [] Cards: { id, row, x (0–1 centre), w (px), kind?: { label, hue }, hue?, icon?, title?, caption?, conditions? }. A step with `conditions` is a condition card.
edges { from: string; to: string }[] [] Connectors from the bottom of `from` to the top of `to`.
sourceIcon "spark" | "package" | "flame" "spark" Icon on the condition record chip.
labels { canvas?, moveHint? } · Canvas name and keyboard-move hint for step cards.

Source 2 files

src/components/viewership/flowchart/layout.ts
/**
 * Flowchart layout math, shared by the server render (estimated heights)
 * and the client script (measured heights, live width, drag offsets).
 * Adapted from Beautiful UI Flowchart (MIT, © 2026 Shane Levine).
 */

const PAD_Y = 24;
const ROW_GAP = 64;
/** Kind pill + gap above a card. */
export const PILL_OFFSET = 30;

export type LayoutNode = { id: string; row: number; x: number; w: number; kind: boolean };
export type Point = { x: number; y: number };

/** Row tops and total canvas height from per-node heights. */
export function rowLayout(nodes: LayoutNode[], heights: Record<string, number>) {
  const rows = [...new Set(nodes.map((n) => n.row))].sort((a, b) => a - b);
  const rowH = rows.map((r) => Math.max(...nodes.filter((n) => n.row === r).map((n) => heights[n.id] ?? 90)));
  const rowY: number[] = [];
  rows.forEach((_, i) => {
    rowY[i] = i === 0 ? PAD_Y : rowY[i - 1] + rowH[i - 1] + ROW_GAP;
  });
  const canvasH = rows.length ? rowY[rows.length - 1] + rowH[rows.length - 1] + PAD_Y : PAD_Y * 2;
  const topOf = (n: LayoutNode) => rowY[rows.indexOf(n.row)];
  return { canvasH, topOf };
}

/** The connector between two anchors: a clamped vertical bezier. */
export function bezier(from: Point, to: Point) {
  const k = Math.min(Math.max(Math.abs(to.y - from.y) * 0.55, 24), 84);
  return `M ${from.x} ${from.y} C ${from.x} ${from.y + k}, ${to.x} ${to.y - k}, ${to.x} ${to.y}`;
}

Ported from Beautiful UI, MIT. Notices.