Skip to content
Viewership Inquire
Astro UI

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

Install

  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.