Skip to content
Brok UI

Loading…

No results

Diagrams

Open source

Dependency-free, token-themed relationship diagrams — flow, network, org chart, indented tree, mind map and fishbone.

Version
v1.1.1
Stability
stable
License
MIT
Related
Organization Chart
Fishbone Diagram
Mind Map
1 of 6

Flow Diagram — A dependency-free static SVG flow diagram: position box, pill and diamond nodes on a column/row grid and connect them with labelled arrowed edges.

Preview

A dependency-free static SVG flow chart. Pass `:nodes` (each `id`, `label`, 0-based `col`, `row`, optional `shape`, `tone`, `description`) and `:edges` (`from`, `to`, optional `label`, `style`, `arrow`, `tone`); arrows leave the right port for a forward edge, elbow into another lane, and loop back through the corridor for a back edge.

previews.components.flow-diagram.default.blade.php Blade
<div class="space-y-4">
    <x-ui.flow-diagram :height="320" label="{{ __('Approval workflow') }}" />

    <p class="text-sm text-muted-foreground">
        {{ __('A dependency-free static SVG flow chart. Pass `:nodes` (each `id`, `label`, 0-based `col`, `row`, optional `shape`, `tone`, `description`) and `:edges` (`from`, `to`, optional `label`, `style`, `arrow`, `tone`); arrows leave the right port for a forward edge, elbow into another lane, and loop back through the corridor for a back edge.') }}
    </p>
</div>

Installation

terminal
php artisan ui:add flow-diagram

Registry contract

php artisan ui:add flow-diagram writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/flow-diagram.blade.php
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

flow-diagram.md
# Brok UI: Flow Diagram (`flow-diagram`)

A dependency-free static SVG flow diagram: position box, pill and diamond nodes on a column/row grid and connect them with labelled arrowed edges.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add flow-diagram
```

## Usage

```blade
<div class="space-y-4">
    <x-ui.flow-diagram :height="320" label="{{ __('Approval workflow') }}" />

    <p class="text-sm text-muted-foreground">
        {{ __('A dependency-free static SVG flow chart. Pass `:nodes` (each `id`, `label`, 0-based `col`, `row`, optional `shape`, `tone`, `description`) and `:edges` (`from`, `to`, optional `label`, `style`, `arrow`, `tone`); arrows leave the right port for a forward edge, elbow into another lane, and loop back through the corridor for a back edge.') }}
    </p>
</div>
```

## Props

- `nodes` (mixed|null, default `null`) — Steps to place: each an object with id, label, col (0-based column), row, and optional shape (box, pill, diamond, circle, parallelogram, hexagon, cylinder, document, subroutine, note), tone, description and dashed.
- `edges` (mixed|null, default `null`) — Connections between nodes: each an object with from, to, and optional label, style (solid, dashed, dotted), arrow (end, both, none) and tone.
- `groups` (array, default `[]`) — Swimlane or subprocess frames drawn around a set of nodes: each an object with label, a list of node ids, and an optional tone.
- `direction` (string, default `lr`) — lr lays columns out left to right; tb lays them top to bottom.
- `height` (int, default `320`) — Minimum drawing height in pixels; the frame grows to fit however many rows it needs.
- `label` (string, default `Flow diagram`) — Accessible name for the figure.

## Use when

- Use when hierarchy, causality, network structure, or process flow is the information itself.
- Documenting a process, decision flow or approval chain with distinct shapes for steps, decisions and terminators.
- Needing swimlane-style groups around related nodes, or labelled arrowed edges between steps.

## Avoid when

- Do not use specialized diagrams when a list, table, or short paragraph would be easier to interpret.
- The structure is a strict hierarchy of one parent to many children; use org-chart instead.
- The nodes have no meaningful order or direction; use network-graph or mind-map instead.

## Anti-patterns

- Using a specialized diagram when linear content is clearer

## Rules

- Use the `<brok:flow-diagram>` tag (or `<x-ui.flow-diagram>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/flow-diagram
- Registry JSON (files, props, contract): https://brokui.dev/r/open/flow-diagram.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

approval.blade.php Blade
{{-- The default request lifecycle framed by chart-card with a step-count badge. --}}
<x-ui.chart-card heading="Approval workflow" subheading="Request lifecycle" trend="5 steps" trend-variant="neutral">
    <x-ui.flow-diagram :height="280" label="{{ __('Approval workflow') }}" />
</x-ui.chart-card>
shapes.blade.php Blade
<x-ui.flow-diagram
    :height="360"
    :nodes="[
        ['id' => 'in', 'label' => __('Order form'), 'description' => __('input'), 'col' => 0, 'row' => 0, 'shape' => 'parallelogram'],
        ['id' => 'prep', 'label' => __('Prepare'), 'description' => __('preparation'), 'col' => 1, 'row' => 0, 'shape' => 'hexagon'],
        ['id' => 'sub', 'label' => __('Charge card'), 'description' => __('subroutine'), 'col' => 2, 'row' => 0, 'shape' => 'subroutine', 'tone' => 'info'],
        ['id' => 'db', 'label' => __('Orders DB'), 'description' => __('database'), 'col' => 2, 'row' => 1, 'shape' => 'cylinder', 'tone' => 'muted'],
        ['id' => 'doc', 'label' => __('Invoice'), 'description' => __('document'), 'col' => 3, 'row' => 0, 'shape' => 'document'],
        ['id' => 'note', 'label' => __('Audit note'), 'description' => __('note'), 'col' => 3, 'row' => 1, 'shape' => 'note', 'tone' => 'warning', 'dashed' => true],
        ['id' => 'end', 'label' => __('End'), 'description' => __('connector'), 'col' => 4, 'row' => 0, 'shape' => 'circle', 'tone' => 'primary'],
    ]"
    :edges="[
        ['from' => 'in', 'to' => 'prep'],
        ['from' => 'prep', 'to' => 'sub'],
        ['from' => 'sub', 'to' => 'db', 'arrow' => 'both', 'style' => 'dotted', 'tone' => 'muted'],
        ['from' => 'sub', 'to' => 'doc', 'tone' => 'success'],
        ['from' => 'sub', 'to' => 'note', 'style' => 'dashed', 'arrow' => 'none', 'tone' => 'warning'],
        ['from' => 'doc', 'to' => 'end'],
    ]"
    label="{{ __('Every node shape and edge style') }}"
/>
vertical.blade.php Blade
<x-ui.flow-diagram
    direction="tb"
    :nodes="[
        ['id' => 'req', 'label' => __('Request'), 'col' => 0, 'row' => 0, 'shape' => 'pill', 'tone' => 'primary'],
        ['id' => 'auth', 'label' => __('Authenticated?'), 'col' => 1, 'row' => 0, 'shape' => 'diamond'],
        ['id' => 'login', 'label' => __('Sign in'), 'col' => 1, 'row' => 1, 'shape' => 'box', 'tone' => 'muted'],
        ['id' => 'handle', 'label' => __('Handle'), 'col' => 2, 'row' => 0, 'shape' => 'box'],
        ['id' => 'resp', 'label' => __('Response'), 'col' => 3, 'row' => 0, 'shape' => 'pill', 'tone' => 'success'],
    ]"
    :edges="[
        ['from' => 'req', 'to' => 'auth'],
        ['from' => 'auth', 'to' => 'handle', 'label' => __('Yes')],
        ['from' => 'auth', 'to' => 'login', 'label' => __('No')],
        ['from' => 'login', 'to' => 'auth', 'style' => 'dashed'],
        ['from' => 'handle', 'to' => 'resp'],
    ]"
    :groups="[['label' => __('Middleware'), 'nodes' => ['auth', 'login']]]"
    label="{{ __('Request lifecycle, top to bottom') }}"
/>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
nodes mixed | null null Steps to place: each an object with id, label, col (0-based column), row, and optional shape (box, pill, diamond, circle, parallelogram, hexagon, cylinder, document, subroutine, note), tone, description and dashed.
edges mixed | null null Connections between nodes: each an object with from, to, and optional label, style (solid, dashed, dotted), arrow (end, both, none) and tone.
groups array [] Swimlane or subprocess frames drawn around a set of nodes: each an object with label, a list of node ids, and an optional tone.
direction string lr lr lays columns out left to right; tb lays them top to bottom.
height int 320 Minimum drawing height in pixels; the frame grows to fit however many rows it needs.
label string Flow diagram Accessible name for the figure.

Slots

  • default — Unused; the diagram renders from the nodes, edges and groups props only.

Data slots

Stable hooks for CSS overrides and browser tests.

flow-diagram flow-diagram-edge flow-diagram-edge-label flow-diagram-group flow-diagram-node

Behavior

  • Renders without arguments using built-in demo data (a small approval process), so it previews immediately.
  • Places nodes on a column/row grid and draws arrowed edges between them, supporting solid, dashed or dotted styles and end/both/no-arrow terminators.
  • Dependency-free SVG: no chart library, computed and laid out entirely in Blade.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Diagram and relationship view

Explain hierarchy, causality, network structure, or flow.

Use when

  • Use when hierarchy, causality, network structure, or process flow is the information itself.
  • Documenting a process, decision flow or approval chain with distinct shapes for steps, decisions and terminators.
  • Needing swimlane-style groups around related nodes, or labelled arrowed edges between steps.

Avoid when

  • Do not use specialized diagrams when a list, table, or short paragraph would be easier to interpret.
  • The structure is a strict hierarchy of one parent to many children; use org-chart instead.
  • The nodes have no meaningful order or direction; use network-graph or mind-map instead.

Use instead

  • List, table, or prose explanation

Anti-patterns

  • Using a specialized diagram when linear content is clearer
Anatomy
root flow-diagram-node flow-diagram-edge flow-diagram-edge-label flow-diagram-group
Theming hooks
flow-diagram flow-diagram-edge flow-diagram-edge-label flow-diagram-group flow-diagram-node

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
No component-owned keyboard interaction; native element behavior applies.
Focus
none
  • The figure carries role="img" with an aria-label from label, so a screen reader hears a named summary instead of parsing raw SVG.
  • Every node and edge label renders as visible SVG text, never as a hover-only tooltip.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Safe

Livewire can update this component through forwarded wire:* attributes.

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/flow-diagram.blade.php Blade
@props([
    // Nodes: each ['id', 'label', 'col' (0-based), 'row', optional 'shape', 'tone',
    //              'description', 'dashed'].
    //   shape: box | pill | diamond | circle | parallelogram | hexagon | cylinder |
    //          document | subroutine | note
    //   tone:  default | primary | info | success | warning | destructive | muted
    'nodes' => null,
    // Edges: each ['from', 'to', optional 'label', 'style' (solid|dashed|dotted),
    //              'arrow' (end|both|none), 'tone'].
    'edges' => null,
    // Groups (swimlanes / subprocess frames): each ['label', 'nodes' => [ids], optional 'tone'].
    'groups' => [],
    // lr lays columns out left → right; tb lays them top → bottom.
    'direction' => 'lr',
    // Minimum drawing height; the frame grows with the rows it has to fit.
    'height' => 320,
    'label' => 'Flow diagram',
])

@php
    // ── Demo data (a small approval process) so it renders without args ──
    $nodes ??= [
        ['id' => 'start', 'label' => __('Start'), 'col' => 0, 'row' => 0, 'shape' => 'pill', 'tone' => 'primary'],
        ['id' => 'validate', 'label' => __('Validate'), 'col' => 1, 'row' => 0, 'shape' => 'box'],
        ['id' => 'decision', 'label' => __('Approved?'), 'col' => 2, 'row' => 0, 'shape' => 'diamond'],
        ['id' => 'approve', 'label' => __('Approve'), 'col' => 3, 'row' => 0, 'shape' => 'box', 'tone' => 'success'],
        ['id' => 'reject', 'label' => __('Reject'), 'col' => 3, 'row' => 1, 'shape' => 'box', 'tone' => 'destructive'],
        ['id' => 'done', 'label' => __('Done'), 'col' => 4, 'row' => 0, 'shape' => 'pill'],
    ];

    $edges ??= [
        ['from' => 'start', 'to' => 'validate'],
        ['from' => 'validate', 'to' => 'decision'],
        ['from' => 'decision', 'to' => 'approve', 'label' => __('Yes')],
        ['from' => 'decision', 'to' => 'reject', 'label' => __('No')],
        ['from' => 'approve', 'to' => 'done'],
        ['from' => 'reject', 'to' => 'validate', 'label' => __('Retry'), 'style' => 'dashed'],
    ];

    $tb = $direction === 'tb';

    // ── Grid ───────────────────────────────────────────────────────────────
    $nodeW = 120;
    $nodeH = 48;
    $withDescription = collect($nodes)->contains(fn ($n) => ! empty($n['description']));
    if ($withDescription) {
        $nodeH = 60;
    }
    $pad = 16;
    $maxCol = $maxRow = 0;
    foreach ($nodes as $n) {
        $maxCol = max($maxCol, (int) $n['col']);
        $maxRow = max($maxRow, (int) $n['row']);
    }

    // Pitch between grid cells along the flow (major) and across it (minor).
    // A back edge is routed in the lane between rows, so the minor pitch
    // leaves room for one.
    $majorPitch = ($tb ? $nodeH : $nodeW) + 64;
    $minorPitch = ($tb ? $nodeW : $nodeH) + 56;

    $majorSpan = $maxCol * $majorPitch + ($tb ? $nodeH : $nodeW);
    $minorSpan = $maxRow * $minorPitch + ($tb ? $nodeW : $nodeH);
    $groupPad = $groups ? 24 : 0;
    $W = $tb ? max(320, $minorSpan + 2 * $pad + 2 * $groupPad) : max(760, $majorSpan + 2 * $pad + 2 * $groupPad);
    $H = $tb ? max((int) $height, $majorSpan + 2 * $pad + 2 * $groupPad) : max((int) $height, $minorSpan + 2 * $pad + 2 * $groupPad);

    // Centre the grid in the frame.
    $offMajor = (($tb ? $H : $W) - $majorSpan) / 2;
    $offMinor = (($tb ? $W : $H) - $minorSpan) / 2;

    $geom = [];
    foreach ($nodes as $n) {
        $u = $offMajor + (int) $n['col'] * $majorPitch + ($tb ? $nodeH : $nodeW) / 2;   // centre along the flow
        $v = $offMinor + (int) $n['row'] * $minorPitch + ($tb ? $nodeW : $nodeH) / 2;   // centre across it
        $cx = $tb ? $v : $u;
        $cy = $tb ? $u : $v;
        $geom[$n['id']] = [
            'cx' => $cx,
            'cy' => $cy,
            'x' => $cx - $nodeW / 2,
            'y' => $cy - $nodeH / 2,
            'col' => (int) $n['col'],
            'row' => (int) $n['row'],
            'shape' => $n['shape'] ?? 'box',
            'tone' => $n['tone'] ?? 'default',
            'label' => $n['label'],
            'description' => $n['description'] ?? null,
            'dashed' => ! empty($n['dashed']),
        ];
    }

    // ── Ports + routing ────────────────────────────────────────────────────
    // Work in flow coordinates (u along the flow, v across it) and map back to
    // x/y at the end, so the same routing serves both directions.
    $toXY = fn (float $u, float $v) => $tb ? [$v, $u] : [$u, $v];
    $halfU = ($tb ? $nodeH : $nodeW) / 2;
    $halfV = ($tb ? $nodeW : $nodeH) / 2;
    $uOf = fn ($g) => $tb ? $g['cy'] : $g['cx'];
    $vOf = fn ($g) => $tb ? $g['cx'] : $g['cy'];

    // Turn a polyline into a path with rounded corners.
    $rounded = function (array $pts, float $r = 8): string {
        $n = count($pts);
        if ($n < 2) {
            return '';
        }
        $d = sprintf('M%.2f %.2f', $pts[0][0], $pts[0][1]);
        for ($i = 1; $i < $n - 1; $i++) {
            [$px, $py] = $pts[$i - 1];
            [$x, $y] = $pts[$i];
            [$nx, $ny] = $pts[$i + 1];
            $l1 = hypot($x - $px, $y - $py);
            $l2 = hypot($nx - $x, $ny - $y);
            $rr = min($r, $l1 / 2, $l2 / 2);
            $ax = $x - ($x - $px) / ($l1 ?: 1) * $rr;
            $ay = $y - ($y - $py) / ($l1 ?: 1) * $rr;
            $bx = $x + ($nx - $x) / ($l2 ?: 1) * $rr;
            $by = $y + ($ny - $y) / ($l2 ?: 1) * $rr;
            $d .= sprintf(' L%.2f %.2f Q%.2f %.2f %.2f %.2f', $ax, $ay, $x, $y, $bx, $by);
        }
        $d .= sprintf(' L%.2f %.2f', $pts[$n - 1][0], $pts[$n - 1][1]);

        return $d;
    };

    // A pair joined in both directions gets each edge nudged to its own side
    // so the two arrows never sit on one line.
    $pairs = [];
    foreach ($edges as $e) {
        $pairs[$e['from'].'→'.$e['to']] = true;
    }

    $paths = [];
    foreach ($edges as $e) {
        if (! isset($geom[$e['from']], $geom[$e['to']])) {
            continue;
        }
        $s = $geom[$e['from']];
        $t = $geom[$e['to']];
        $su = $uOf($s); $sv = $vOf($s);
        $tu = $uOf($t); $tv = $vOf($t);
        $dc = $t['col'] - $s['col'];
        $dr = $t['row'] - $s['row'];
        $twoWay = isset($pairs[$e['to'].'→'.$e['from']]);
        $nudge = $twoWay ? (($dc > 0 || ($dc === 0 && $dr > 0)) ? -8 : 8) : 0;

        if ($dc > 0 && $dr === 0) {
            // Forward, same lane: a straight run from the out port to the in port.
            $pts = [[$su + $halfU, $sv + $nudge], [$tu - $halfU, $tv + $nudge]];
        } elseif ($dc === 0 && $dr !== 0) {
            // Same column: straight across the lanes.
            $dir = $dr > 0 ? 1 : -1;
            $pts = [[$su + $nudge, $sv + $dir * $halfV], [$tu + $nudge, $tv - $dir * $halfV]];
        } elseif ($dc > 0) {
            // Forward into another lane: out port → elbow midway → in port.
            $mid = ($su + $halfU + $tu - $halfU) / 2;
            $pts = [[$su + $halfU, $sv], [$mid, $sv], [$mid, $tv], [$tu - $halfU, $tv]];
        } else {
            // Back edge (loop): leave across the lane, run back along the
            // corridor beside the source's lane, and enter the target across
            // its own lane so it never crosses the forward path.
            $dir = $sv >= $tv ? 1 : -1;
            $corridor = $sv + $dir * ($halfV + 20);
            $pts = [[$su, $sv + $dir * $halfV], [$su, $corridor], [$tu, $corridor], [$tu, $tv + $dir * $halfV]];
            if ($dr === 0) {
                // Same lane: enter the target from the corridor side as well.
                $pts = [[$su, $sv + $halfV], [$su, $sv + $halfV + 20], [$tu, $sv + $halfV + 20], [$tu, $tv + $halfV]];
            }
        }

        $xy = array_map(fn ($p) => $toXY($p[0], $p[1]), $pts);

        // Label on the midpoint of the longest segment; a straight two-point
        // edge carries it just beside the line so the chip never hides the
        // arrowhead of a short run.
        $best = 0; $lx = $ly = 0; $horizontal = false;
        for ($i = 1; $i < count($xy); $i++) {
            $len = hypot($xy[$i][0] - $xy[$i - 1][0], $xy[$i][1] - $xy[$i - 1][1]);
            if ($len >= $best) {
                $best = $len;
                $lx = ($xy[$i][0] + $xy[$i - 1][0]) / 2;
                $ly = ($xy[$i][1] + $xy[$i - 1][1]) / 2;
                $horizontal = abs($xy[$i][1] - $xy[$i - 1][1]) < 0.5;
            }
        }
        if (count($xy) === 2) {
            if ($horizontal) {
                $ly -= 14;
            } else {
                $lx += (mb_strlen((string) ($e['label'] ?? '')) * 6.4 + 14) / 2 + 6;
            }
        }

        $style = $e['style'] ?? 'solid';
        $paths[] = [
            'd' => $rounded($xy),
            'label' => $e['label'] ?? null,
            'lx' => $lx,
            'ly' => $ly,
            'lw' => mb_strlen((string) ($e['label'] ?? '')) * 6.4 + 14,
            'dash' => $style === 'dashed' ? '6 4' : ($style === 'dotted' ? '1.5 4' : null),
            'arrow' => $e['arrow'] ?? 'end',
            'tone' => $e['tone'] ?? 'default',
        ];
    }

    // ── Groups: a dashed frame around the bounding box of its members ──────
    $frames = [];
    foreach ((array) $groups as $g) {
        $members = array_values(array_filter((array) ($g['nodes'] ?? []), fn ($id) => isset($geom[$id])));
        if (! $members) {
            continue;
        }
        $x0 = min(array_map(fn ($id) => $geom[$id]['x'], $members)) - 16;
        $y0 = min(array_map(fn ($id) => $geom[$id]['y'], $members)) - 28;
        $x1 = max(array_map(fn ($id) => $geom[$id]['x'] + $nodeW, $members)) + 16;
        $y1 = max(array_map(fn ($id) => $geom[$id]['y'] + $nodeH, $members)) + 16;
        $frames[] = ['x' => $x0, 'y' => $y0, 'w' => $x1 - $x0, 'h' => $y1 - $y0, 'label' => $g['label'] ?? '', 'tone' => $g['tone'] ?? 'muted'];
    }

    // ── Tone recipes (token utilities only) ────────────────────────────────
    $nodeTones = [
        'default' => ['shape' => 'fill-card stroke-border', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
        'primary' => ['shape' => 'fill-primary stroke-primary', 'text' => 'fill-primary-foreground', 'sub' => 'fill-primary-foreground/70'],
        'info' => ['shape' => 'fill-info/10 stroke-info', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
        'success' => ['shape' => 'fill-success/10 stroke-success', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
        'warning' => ['shape' => 'fill-warning/10 stroke-warning', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
        'destructive' => ['shape' => 'fill-destructive/10 stroke-destructive', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
        'muted' => ['shape' => 'fill-muted stroke-border', 'text' => 'fill-muted-foreground', 'sub' => 'fill-muted-foreground'],
    ];
    $edgeTones = [
        'default' => 'text-muted-foreground',
        'primary' => 'text-primary',
        'info' => 'text-info',
        'success' => 'text-success',
        'warning' => 'text-warning',
        'destructive' => 'text-destructive',
        'muted' => 'text-border',
    ];
    $usedEdgeTones = array_values(array_unique(array_map(fn ($p) => array_key_exists($p['tone'], $edgeTones) ? $p['tone'] : 'default', $paths)));

    $uid = substr(md5($label.$W.$H.count($nodes)), 0, 8);

    // Shape outlines. Every shape is drawn inside the node's box so the ports
    // (box edges) stay correct for all of them.
    $shapePath = function (array $g) use ($nodeW, $nodeH): array {
        $x = $g['x']; $y = $g['y']; $w = $nodeW; $h = $nodeH; $cx = $g['cx']; $cy = $g['cy'];
        $f = fn (float $v) => round($v, 2);

        return match ($g['shape']) {
            'diamond' => ['polygon', sprintf('%s %s %s %s %s %s %s %s', $f($cx), $f($y), $f($x + $w), $f($cy), $f($cx), $f($y + $h), $f($x), $f($cy))],
            'parallelogram' => ['polygon', sprintf('%s %s %s %s %s %s %s %s', $f($x + 14), $f($y), $f($x + $w), $f($y), $f($x + $w - 14), $f($y + $h), $f($x), $f($y + $h))],
            'hexagon' => ['polygon', sprintf('%s %s %s %s %s %s %s %s %s %s %s %s', $f($x + 14), $f($y), $f($x + $w - 14), $f($y), $f($x + $w), $f($cy), $f($x + $w - 14), $f($y + $h), $f($x + 14), $f($y + $h), $f($x), $f($cy))],
            'circle' => ['circle', ''],
            'cylinder' => ['path', sprintf('M%s %s v%s a%s 7 0 0 0 %s 0 v-%s a%s 7 0 0 0 -%s 0 a%s 7 0 0 0 %s 0', $f($x), $f($y + 7), $f($h - 14), $f($w / 2), $f($w), $f($h - 14), $f($w / 2), $f($w), $f($w / 2), $f($w))],
            'document' => ['path', sprintf('M%s %s h%s v%s q-%s -12 -%s 0 t-%s 0 z', $f($x), $f($y), $f($w), $f($h - 6), $f($w / 4), $f($w / 2), $f($w / 2))],
            'note' => ['path', sprintf('M%s %s h%s l12 12 v%s h-%s z', $f($x), $f($y), $f($w - 12), $f($h - 12), $f($w))],
            default => ['rect', ''],
        };
    };
@endphp

<div
    data-slot="flow-diagram"
    data-direction="{{ $tb ? 'tb' : 'lr' }}"
    role="img"
    aria-label="{{ $label }}"
    {{ $attributes->merge(['class' => 'relative w-full text-foreground']) }}
>
    {{-- A top-to-bottom chart keeps its natural width (centred, capped at the
         container) instead of stretching a narrow column to full width. --}}
    <svg
        viewBox="0 0 {{ round($W, 2) }} {{ round($H, 2) }}"
        width="{{ $tb ? round($W) : '100%' }}"
        height="{{ round($H) }}"
        preserveAspectRatio="xMidYMid meet"
        class="{{ $tb ? 'mx-auto h-auto max-w-full' : 'h-auto w-full' }}"
        fill="none"
        aria-hidden="true"
    >
        <defs>
            {{-- One arrowhead per edge tone. `userSpaceOnUse` keeps the head the
                 same size whatever the stroke; refX puts the tip on the port. --}}
            @foreach ($usedEdgeTones as $tone)
                <marker
                    id="flow-{{ $uid }}-{{ $tone }}"
                    viewBox="0 0 10 10"
                    refX="9"
                    refY="5"
                    markerWidth="10"
                    markerHeight="10"
                    markerUnits="userSpaceOnUse"
                    orient="auto-start-reverse"
                    class="{{ $edgeTones[$tone] }}"
                >
                    <path d="M1 1 L9 5 L1 9 z" fill="currentColor" stroke="currentColor" stroke-width="1" stroke-linejoin="round" />
                </marker>
            @endforeach
        </defs>

        {{-- Group frames (behind everything). --}}
        @foreach ($frames as $frame)
            <g data-slot="flow-diagram-group">
                <rect
                    class="{{ $frame['tone'] === 'muted' ? 'fill-muted/40 stroke-border' : ($nodeTones[$frame['tone']]['shape'] ?? 'fill-muted/40 stroke-border') }}"
                    x="{{ round($frame['x'], 2) }}"
                    y="{{ round($frame['y'], 2) }}"
                    width="{{ round($frame['w'], 2) }}"
                    height="{{ round($frame['h'], 2) }}"
                    rx="12"
                    stroke-width="1"
                    stroke-dasharray="4 4"
                />
                <text
                    class="fill-muted-foreground text-2xs font-medium uppercase tracking-wide"
                    x="{{ round($frame['x'] + 12, 2) }}"
                    y="{{ round($frame['y'] + 14, 2) }}"
                    dominant-baseline="middle"
                >{{ $frame['label'] }}</text>
            </g>
        @endforeach

        {{-- Edges (behind the nodes). --}}
        @foreach ($paths as $p)
            @php($tone = array_key_exists($p['tone'], $edgeTones) ? $p['tone'] : 'default')
            <path
                data-slot="flow-diagram-edge"
                class="{{ $edgeTones[$tone] }}"
                d="{{ $p['d'] }}"
                stroke="currentColor"
                stroke-width="1.5"
                stroke-linecap="round"
                stroke-linejoin="round"
                fill="none"
                @if ($p['dash']) stroke-dasharray="{{ $p['dash'] }}" @endif
                @if ($p['arrow'] !== 'none') marker-end="url(#flow-{{ $uid }}-{{ $tone }})" @endif
                @if ($p['arrow'] === 'both') marker-start="url(#flow-{{ $uid }}-{{ $tone }})" @endif
            />
            @if ($p['label'])
                <rect
                    class="fill-background stroke-border"
                    x="{{ round($p['lx'] - $p['lw'] / 2, 2) }}"
                    y="{{ round($p['ly'] - 10, 2) }}"
                    width="{{ round($p['lw'], 2) }}"
                    height="20"
                    rx="10"
                    stroke-width="1"
                />
                <text
                    data-slot="flow-diagram-edge-label"
                    class="pointer-events-none fill-muted-foreground text-2xs font-medium"
                    x="{{ round($p['lx'], 2) }}"
                    y="{{ round($p['ly'], 2) }}"
                    text-anchor="middle"
                    dominant-baseline="middle"
                >{{ $p['label'] }}</text>
            @endif
        @endforeach

        {{-- Nodes. --}}
        @foreach ($nodes as $n)
            @php($g = $geom[$n['id']])
            @php($tone = $nodeTones[$g['tone']] ?? $nodeTones['default'])
            @php([$kind, $data] = $shapePath($g))
            <g data-slot="flow-diagram-node" data-shape="{{ $g['shape'] }}" data-tone="{{ $g['tone'] }}">
                @if ($kind === 'polygon')
                    <polygon class="{{ $tone['shape'] }}" points="{{ $data }}" stroke-width="1.5" stroke-linejoin="round" @if ($g['dashed']) stroke-dasharray="5 3" @endif />
                @elseif ($kind === 'circle')
                    <circle class="{{ $tone['shape'] }}" cx="{{ round($g['cx'], 2) }}" cy="{{ round($g['cy'], 2) }}" r="{{ round($nodeH / 2, 2) }}" stroke-width="1.5" @if ($g['dashed']) stroke-dasharray="5 3" @endif />
                @elseif ($kind === 'path')
                    <path class="{{ $tone['shape'] }}" d="{{ $data }}" stroke-width="1.5" stroke-linejoin="round" @if ($g['dashed']) stroke-dasharray="5 3" @endif />
                @else
                    <rect
                        class="{{ $tone['shape'] }}"
                        x="{{ round($g['x'], 2) }}"
                        y="{{ round($g['y'], 2) }}"
                        width="{{ $nodeW }}"
                        height="{{ $nodeH }}"
                        rx="{{ $g['shape'] === 'pill' ? round($nodeH / 2, 2) : 8 }}"
                        stroke-width="1.5"
                        @if ($g['dashed']) stroke-dasharray="5 3" @endif
                    />
                    @if ($g['shape'] === 'subroutine')
                        <line class="{{ $tone['shape'] }}" x1="{{ round($g['x'] + 10, 2) }}" y1="{{ round($g['y'], 2) }}" x2="{{ round($g['x'] + 10, 2) }}" y2="{{ round($g['y'] + $nodeH, 2) }}" stroke-width="1.5" />
                        <line class="{{ $tone['shape'] }}" x1="{{ round($g['x'] + $nodeW - 10, 2) }}" y1="{{ round($g['y'], 2) }}" x2="{{ round($g['x'] + $nodeW - 10, 2) }}" y2="{{ round($g['y'] + $nodeH, 2) }}" stroke-width="1.5" />
                    @endif
                @endif
                <text
                    class="pointer-events-none {{ $tone['text'] }} text-xs font-medium"
                    x="{{ round($g['cx'], 2) }}"
                    y="{{ round($g['description'] ? $g['cy'] - 7 : $g['cy'], 2) }}"
                    text-anchor="middle"
                    dominant-baseline="middle"
                >{{ $g['label'] }}</text>
                @if ($g['description'])
                    <text
                        class="pointer-events-none {{ $tone['sub'] }} text-2xs"
                        x="{{ round($g['cx'], 2) }}"
                        y="{{ round($g['cy'] + 9, 2) }}"
                        text-anchor="middle"
                        dominant-baseline="middle"
                    >{{ $g['description'] }}</text>
                @endif
            </g>
        @endforeach
    </svg>
</div>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.1
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0