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
Indented Tree
Mind Map
Flow Diagram
3 of 6

Organization Chart — Dependency-free static SVG organization chart that lays out a nested name/role tree top-down with orthogonal reporting-line connectors.

Preview

A dependency-free static SVG org chart. Pass a nested `:tree` node — `name`, `role`, and a `children[]` array of the same shape — and it lays the hierarchy out top-down with orthogonal reporting-line connectors.

previews.components.org-chart.default.blade.php Blade
<div class="space-y-4">
    <x-ui.org-chart :height="360" label="{{ __('Organization chart') }}" />

    <p class="text-sm text-muted-foreground">
        {{ __('A dependency-free static SVG org chart. Pass a nested `:tree` node — `name`, `role`, and a `children[]` array of the same shape — and it lays the hierarchy out top-down with orthogonal reporting-line connectors.') }}
    </p>
</div>

Installation

terminal
php artisan ui:add org-chart

Registry contract

php artisan ui:add org-chart 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/org-chart.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.

org-chart.md
# Brok UI: Organization Chart (`org-chart`)

Dependency-free static SVG organization chart that lays out a nested name/role tree top-down with orthogonal reporting-line connectors.

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

## Install

```bash
php artisan ui:add org-chart
```

## Usage

```blade
<div class="space-y-4">
    <x-ui.org-chart :height="360" label="{{ __('Organization chart') }}" />

    <p class="text-sm text-muted-foreground">
        {{ __('A dependency-free static SVG org chart. Pass a nested `:tree` node — `name`, `role`, and a `children[]` array of the same shape — and it lays the hierarchy out top-down with orthogonal reporting-line connectors.') }}
    </p>
</div>
```

## Props

- `tree` (mixed|null, default `null`) — A nested node tree: an object with name, role, optional department, optional avatar image URL, and a children list of the same shape. Defaults to a demo executive tree.
- `height` (int, default `360`) — Minimum SVG height in pixels; the frame grows to fit however many levels it needs.
- `label` (string, default `Organization chart`) — Accessible name for the figure.
- `showRole` (bool, default `true`) — Shows each person's role under their name.
- `showDepartment` (bool, default `false`) — Shows each person's department on their card.
- `showAvatar` (bool, default `false`) — Shows a circle with the person's initials, or their avatar image, at the start of the card.
- `showReports` (bool, default `false`) — Shows a small badge with the number of direct reports under managers.

## Use when

- Use when hierarchy, causality, network structure, or process flow is the information itself.
- Showing a reporting structure of people or teams, top-down, one manager to many direct reports.
- Needing name/role cards connected by orthogonal reporting lines, optionally with avatars, department and report counts.

## Avoid when

- Do not use specialized diagrams when a list, table, or short paragraph would be easier to interpret.
- The hierarchy is files or settings, not people; use indented-tree instead.
- The relationships are a general graph or a free-form brainstorm rather than a strict tree; use network-graph or mind-map instead.

## Anti-patterns

- Using a specialized diagram when linear content is clearer

## Rules

- Use the `<brok:org-chart>` tag (or `<x-ui.org-chart>`) 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/org-chart
- Registry JSON (files, props, contract): https://brokui.dev/r/open/org-chart.json

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

Examples

detailed.blade.php Blade
<x-ui.org-chart :height="400" show-avatar show-department show-reports label="{{ __('Organization chart with avatars, departments and report counts') }}" />
team.blade.php Blade
{{-- The default reporting tree framed by chart-card with a headcount badge. --}}
<x-ui.chart-card heading="Team structure" subheading="Reporting lines" trend="8 people" trend-variant="neutral">
    <x-ui.org-chart :height="360" label="{{ __('Team structure') }}" />
</x-ui.chart-card>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
tree mixed | null null A nested node tree: an object with name, role, optional department, optional avatar image URL, and a children list of the same shape. Defaults to a demo executive tree.
height int 360 Minimum SVG height in pixels; the frame grows to fit however many levels it needs.
label string Organization chart Accessible name for the figure.
showRole bool true Shows each person's role under their name.
showDepartment bool false Shows each person's department on their card.
showAvatar bool false Shows a circle with the person's initials, or their avatar image, at the start of the card.
showReports bool false Shows a small badge with the number of direct reports under managers.

Slots

  • default — Unused; the chart renders from the tree prop only.

Data slots

Stable hooks for CSS overrides and browser tests.

org-chart org-chart-edge org-chart-node

Behavior

  • Renders without arguments using built-in demo data, so it previews immediately.
  • Lays the tree out top-down with each level centred under its parent, connected by orthogonal reporting-line connectors.
  • 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.
  • Showing a reporting structure of people or teams, top-down, one manager to many direct reports.
  • Needing name/role cards connected by orthogonal reporting lines, optionally with avatars, department and report counts.

Avoid when

  • Do not use specialized diagrams when a list, table, or short paragraph would be easier to interpret.
  • The hierarchy is files or settings, not people; use indented-tree instead.
  • The relationships are a general graph or a free-form brainstorm rather than a strict tree; 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 org-chart-node org-chart-edge
Theming hooks
org-chart org-chart-edge org-chart-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 name, role and department 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/org-chart.blade.php Blade
@props([
    // A nested node tree: ['name', 'role', optional 'department', 'avatar' (image
    // URL) and 'children' => [...same shape]].
    'tree' => null,
    'height' => 360,
    'label' => 'Organization chart',
    // What each card shows besides the name.
    'showRole' => true,
    'showDepartment' => false,
    // A circle with the person's initials (or their `avatar` image) at the start of the card.
    'showAvatar' => false,
    // A small badge with the number of direct reports under managers.
    'showReports' => false,
])

@php
    // ── Demo data so it renders without args ─────────────────────────────────
    $tree ??= [
        'name' => __('Avery Chen'),
        'role' => __('CEO'),
        'department' => __('Executive'),
        'children' => [
            [
                'name' => __('Jordan Blake'),
                'department' => __('Engineering'),
                'role' => __('VP Engineering'),
                'children' => [
                    ['name' => __('Sam Rivera'), 'department' => __('Engineering'), 'role' => __('Backend Lead')],
                    ['name' => __('Priya Nair'), 'department' => __('Engineering'), 'role' => __('Frontend Lead')],
                ],
            ],
            [
                'name' => __('Morgan Diaz'),
                'department' => __('Sales'),
                'role' => __('VP Sales'),
                'children' => [
                    ['name' => __('Lee Park'), 'department' => __('Sales'), 'role' => __('Account Executive')],
                    ['name' => __('Robin Cole'), 'department' => __('Sales'), 'role' => __('Sales Engineer')],
                ],
            ],
            [
                'name' => __('Taylor Quinn'),
                'department' => __('Operations'),
                'role' => __('VP Operations'),
                'children' => [
                    ['name' => __('Casey Wong'), 'department' => __('Operations'), 'role' => __('People Ops')],
                    ['name' => __('Drew Patel'), 'department' => __('Operations'), 'role' => __('Finance Lead')],
                ],
            ],
        ],
    ];

    $showRole = filter_var($showRole, FILTER_VALIDATE_BOOLEAN);
    $showDepartment = filter_var($showDepartment, FILTER_VALIDATE_BOOLEAN);
    $showAvatar = filter_var($showAvatar, FILTER_VALIDATE_BOOLEAN);
    $showReports = filter_var($showReports, FILTER_VALIDATE_BOOLEAN);

    // ── Geometry ─────────────────────────────────────────────────────────────
    $lines = 1 + ($showRole ? 1 : 0) + ($showDepartment ? 1 : 0);
    $boxW = $showAvatar ? 188 : 168;             // node box width
    $boxH = max($showAvatar ? 64 : 0, 24 + $lines * 17);   // node box height grows with the lines it carries
    $slotW = $boxW + 28;                          // horizontal slot per leaf
    $avatarR = 18;
    $textX = $showAvatar ? 16 + 2 * $avatarR + 12 : 0;   // text start (from the box's left edge) when an avatar leads

    $initials = function (string $name): string {
        $parts = preg_split('/\s+/', trim($name)) ?: [];
        $first = mb_substr($parts[0] ?? '', 0, 1);
        $last = count($parts) > 1 ? mb_substr(end($parts), 0, 1) : '';

        return mb_strtoupper($first.$last);
    };
    $levelGap = max(96, (int) round((int) $height / 4)); // vertical distance between levels
    $pad = 16;     // viewBox padding

    // ── Tidy top-down layout ──────────────────────────────────────────────────
    // Walk the tree depth-first. Each LEAF takes the next x slot; a parent's x is
    // the midpoint of its children. y is depth * levelGap. Collect flat nodes +
    // parent→child edges for orthogonal (elbow) connectors.
    $nodes = [];
    $edges = [];
    $leafCursor = 0;

    $layout = function (array $node, int $depth, ?int $parentIndex) use (
        &$layout, &$nodes, &$edges, &$leafCursor, $slotW, $levelGap
    ) {
        $children = $node['children'] ?? [];
        $index = count($nodes);
        // Reserve this node's slot in the flat list before recursing so children
        // can reference their parent by index.
        $nodes[$index] = [
            'name' => $node['name'] ?? '',
            'role' => $node['role'] ?? '',
            'department' => $node['department'] ?? '',
            'avatar' => $node['avatar'] ?? null,
            'reports' => count($children),
            'x' => 0.0,
            'y' => $depth * $levelGap,
        ];

        if ($parentIndex !== null) {
            $edges[] = ['parent' => $parentIndex, 'child' => $index];
        }

        if (empty($children)) {
            // Leaf: claim the next horizontal slot (centre of the slot).
            $nodes[$index]['x'] = $leafCursor * $slotW + $slotW / 2;
            $leafCursor++;
            return $nodes[$index]['x'];
        }

        $childXs = [];
        foreach ($children as $child) {
            $childXs[] = $layout($child, $depth + 1, $index);
        }
        // Parent centred over the span of its children.
        $nodes[$index]['x'] = (min($childXs) + max($childXs)) / 2;
        return $nodes[$index]['x'];
    };

    $layout($tree, 0, null);

    // ── viewBox extents (computed from the laid-out nodes) ────────────────────
    $maxX = 0.0;
    $maxY = 0.0;
    foreach ($nodes as $n) {
        $maxX = max($maxX, $n['x'] + $boxW / 2);
        $maxY = max($maxY, $n['y'] + $boxH);
    }
    $vbW = $maxX + $boxW / 2 + $pad; // half a box of breathing room each side
    $vbH = $maxY + $pad;

    // Half-step where the horizontal bus sits between two levels.
    $busDrop = (int) round($levelGap / 2);
@endphp

<div
    data-slot="org-chart"
    role="img"
    aria-label="{{ $label }}"
    {{-- Wide charts scroll horizontally; keep the scroll region keyboard-focusable. --}}
    tabindex="0"
    {{ $attributes->merge(['class' => 'w-full overflow-x-auto text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring']) }}
>
    <svg
        viewBox="0 0 {{ round($vbW, 2) }} {{ round($vbH, 2) }}"
        width="100%"
        height="{{ (int) $height }}"
        preserveAspectRatio="xMidYMin meet"
        fill="none"
        aria-hidden="true"
        class="h-auto w-full min-w-(--org-chart-min-w)"
        style="--org-chart-min-w: {{ (int) round($vbW) }}px"
    >
        {{-- Connectors: parent bottom-centre → horizontal bus → child top-centre. --}}
        @foreach ($edges as $edge)
            @php($p = $nodes[$edge['parent']])
            @php($c = $nodes[$edge['child']])
            @php($pBottomY = $p['y'] + $boxH)
            @php($cTopY = $c['y'])
            @php($busY = $cTopY - $busDrop)
            <path
                data-slot="org-chart-edge"
                class="stroke-border"
                stroke="currentColor"
                stroke-width="1.5"
                stroke-linecap="round"
                stroke-linejoin="round"
                d="M{{ round($p['x'], 2) }} {{ round($pBottomY, 2) }} V{{ round($busY, 2) }} H{{ round($c['x'], 2) }} V{{ round($cTopY, 2) }}"
            />
        @endforeach

        {{-- Nodes: rounded card box + avatar / name / role / department / reports. --}}
        @foreach ($nodes as $i => $n)
            @php($nx = $n['x'] - $boxW / 2)
            @php($ny = $n['y'])
            @php($tx = $showAvatar ? $nx + $textX : $n['x'])
            @php($anchor = $showAvatar ? 'start' : 'middle')
            @php($firstY = $ny + ($boxH - ($lines - 1) * 17) / 2)
            <g data-slot="org-chart-node">
                <rect
                    class="fill-card stroke-border"
                    x="{{ round($nx, 2) }}"
                    y="{{ round($ny, 2) }}"
                    width="{{ $boxW }}"
                    height="{{ $boxH }}"
                    rx="10"
                    fill="currentColor"
                    stroke="currentColor"
                    stroke-width="1"
                />
                @if ($showAvatar)
                    @php($ax = $nx + 16 + $avatarR)
                    @php($ay = $ny + $boxH / 2)
                    @if ($n['avatar'])
                        <clipPath id="org-avatar-{{ $i }}-{{ substr(md5($label), 0, 6) }}"><circle cx="{{ round($ax, 2) }}" cy="{{ round($ay, 2) }}" r="{{ $avatarR }}" /></clipPath>
                        <image
                            href="{{ $n['avatar'] }}"
                            x="{{ round($ax - $avatarR, 2) }}"
                            y="{{ round($ay - $avatarR, 2) }}"
                            width="{{ 2 * $avatarR }}"
                            height="{{ 2 * $avatarR }}"
                            preserveAspectRatio="xMidYMid slice"
                            clip-path="url(#org-avatar-{{ $i }}-{{ substr(md5($label), 0, 6) }})"
                        />
                    @else
                        <circle class="fill-muted" cx="{{ round($ax, 2) }}" cy="{{ round($ay, 2) }}" r="{{ $avatarR }}" fill="currentColor" />
                        <text
                            class="pointer-events-none fill-foreground text-2xs font-semibold"
                            x="{{ round($ax, 2) }}"
                            y="{{ round($ay, 2) }}"
                            text-anchor="middle"
                            dominant-baseline="central"
                        >{{ $initials($n['name']) }}</text>
                    @endif
                @endif
                <text
                    class="pointer-events-none fill-foreground text-xs font-semibold"
                    x="{{ round($tx, 2) }}"
                    y="{{ round($firstY, 2) }}"
                    text-anchor="{{ $anchor }}"
                    dominant-baseline="middle"
                >{{ $n['name'] }}</text>
                @php($lineY = $firstY)
                @if ($showRole)
                    @php($lineY += 17)
                    <text
                        class="pointer-events-none fill-muted-foreground text-2xs"
                        x="{{ round($tx, 2) }}"
                        y="{{ round($lineY, 2) }}"
                        text-anchor="{{ $anchor }}"
                        dominant-baseline="middle"
                    >{{ $n['role'] }}</text>
                @endif
                @if ($showDepartment)
                    @php($lineY += 17)
                    <text
                        class="pointer-events-none fill-muted-foreground text-2xs font-medium uppercase tracking-wide"
                        x="{{ round($tx, 2) }}"
                        y="{{ round($lineY, 2) }}"
                        text-anchor="{{ $anchor }}"
                        dominant-baseline="middle"
                    >{{ $n['department'] }}</text>
                @endif
                @if ($showReports && $n['reports'] > 0)
                    {{-- Direct-report count, straddling the bottom edge. --}}
                    <rect
                        class="fill-background stroke-border"
                        x="{{ round($n['x'] - 12, 2) }}"
                        y="{{ round($ny + $boxH - 9, 2) }}"
                        width="24"
                        height="18"
                        rx="9"
                        stroke-width="1"
                    />
                    <text
                        class="pointer-events-none fill-muted-foreground text-2xs font-semibold tabular-nums"
                        x="{{ round($n['x'], 2) }}"
                        y="{{ round($ny + $boxH, 2) }}"
                        text-anchor="middle"
                        dominant-baseline="central"
                    >{{ $n['reports'] }}</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