diagram
HTML/CSS block diagrams — rows, blocks with variants and tooltips, diamonds, brackets, notes, and a legend. Real DOM: selectable, screen-reader reachable, reflows.
diagramlayoutaccessibility
Preview
request
Clientbrowser
tlsEdgeTerminates TLS, caches static assets
Originapp server
failure
Queue
Dead letter
(unused)
Retries stop after the third attempt.
retry path
Every label here is selectable text.
hot pathfailureoptional
Installation
shadcn CLI
Terminal
npx shadcn@latest add https://mocoui.site/r/diagram.jsonManual
Copy each file from the Source section into its target path:
registry/diagram/diagram.tsx → components/moco/diagram.tsx
registry/diagram/diagram.css → components/moco/diagram.cssNeeds moco items: tokens
Usage
usage.tsx
import * as React from "react";
import {
Dgm,
DgmRow,
DgmBlock,
DgmDiamond,
DgmBracket,
DgmNote,
DgmLegend,
} from "./diagram";
export default function DiagramDemo() {
return (
<div style={{ overflowX: "auto" }}>
<Dgm minWidth={520}>
<DgmRow label="request">
<DgmBlock label="Client" sub="browser" />
<DgmDiamond label="tls" />
<DgmBlock label="Edge" variant="active" tip="Terminates TLS, caches static assets" />
<DgmBlock label="Origin" sub="app server" grow={2} />
</DgmRow>
<DgmRow label="failure" note="Retries stop after the third attempt.">
<DgmBlock label="Queue" variant="dashed" />
<DgmBlock label="Dead letter" variant="warn" />
<DgmBlock label="(unused)" variant="ghost" />
</DgmRow>
<DgmBracket span={2} offset={1} total={3}>
retry path
</DgmBracket>
<DgmNote>Every label here is selectable text.</DgmNote>
<DgmLegend
items={[
{ color: "accent", label: "hot path" },
{ color: "warn", label: "failure" },
{ color: "faint", label: "optional" },
]}
/>
</Dgm>
</div>
);
}
Source
components/moco/diagram.tsx
import * as React from "react";
import "./diagram.css";
/**
* HTML/CSS block diagrams. Real DOM, not SVG and not images: every label is
* selectable text, reachable by a screen reader, and reflows on mobile.
*/
export interface DgmProps {
children: React.ReactNode;
minWidth?: number;
live?: boolean;
}
export function Dgm({ children, minWidth, live }: DgmProps) {
return (
<div
className="moco-dgm"
style={minWidth ? { minWidth } : undefined}
role="group"
aria-live={live ? "polite" : undefined}
>
{children}
</div>
);
}
export interface DgmRowProps {
label?: string;
children: React.ReactNode;
note?: React.ReactNode;
}
export function DgmRow({ label, children, note }: DgmRowProps) {
return (
<>
<div className="moco-dgm-row">
<div className="moco-dgm-row-label">{label}</div>
<div className="moco-dgm-track">{children}</div>
</div>
{note ? (
<div className="moco-dgm-row" style={{ marginTop: "-0.7rem" }}>
<div />
<div className="moco-dgm-note">{note}</div>
</div>
) : null}
</>
);
}
export type BlockVariant = "default" | "active" | "dashed" | "warn" | "ghost";
export interface DgmBlockProps {
label: React.ReactNode;
sub?: React.ReactNode;
variant?: BlockVariant;
tip?: string;
grow?: number;
id?: string;
}
export function DgmBlock({
label,
sub,
variant = "default",
tip,
grow = 1,
id,
}: DgmBlockProps) {
return (
<div
id={id}
className="moco-dgm-block"
data-variant={variant}
style={{ flexGrow: grow }}
tabIndex={tip ? 0 : undefined}
title={tip}
>
<span className="moco-b-label">{label}</span>
{sub ? <span className="moco-b-sub">{sub}</span> : null}
{tip ? <span className="moco-dgm-tip">{tip}</span> : null}
</div>
);
}
export function DgmDiamond({ label }: { label?: string }) {
return (
<span
style={{ display: "inline-flex", alignItems: "center", flex: "none" }}
aria-label={label ? `marker: ${label}` : "marker"}
>
<span className="moco-dgm-diamond" />
{label ? (
<span style={{ fontSize: 11.5, color: "var(--moco-faint)" }}>{label}</span>
) : null}
</span>
);
}
export interface DgmBracketProps {
children: React.ReactNode;
/** How many block-widths the bracket covers. */
span?: number;
/** How many block-widths to skip before it starts. */
offset?: number;
/** Total block-widths in the row above. */
total?: number;
}
export function DgmBracket({ children, span = 1, offset = 0, total = 1 }: DgmBracketProps) {
return (
<div className="moco-dgm-row">
<div />
<div style={{ display: "flex" }}>
{offset > 0 ? <div style={{ flex: offset }} /> : null}
<div style={{ flex: span }}>
<div className="moco-dgm-bracket" />
<div className="moco-dgm-bracket-cap">{children}</div>
</div>
{total - span - offset > 0 ? <div style={{ flex: total - span - offset }} /> : null}
</div>
</div>
);
}
export function DgmNote({ children }: { children: React.ReactNode }) {
return (
<div className="moco-dgm-row">
<div />
<div className="moco-dgm-note">{children}</div>
</div>
);
}
export interface DgmLegendProps {
items: { color: "accent" | "warn" | "faint" | "ink" | "line"; label: string }[];
}
export function DgmLegend({ items }: DgmLegendProps) {
return (
<div className="moco-dgm-legend">
{items.map((it) => (
<span
key={it.label}
style={{
color: it.color === "accent" ? "var(--moco-accent-dk)" : `var(--moco-${it.color})`,
}}
>
{/* The lime chip matches the filled block it stands for, but takes a
dark border — on paper the lime alone has no edge. */}
<span
className="moco-sw"
style={{
background: it.color === "accent" ? "var(--moco-accent)" : "transparent",
borderColor: it.color === "accent" ? "var(--moco-accent-dk)" : undefined,
}}
/>
<span style={{ color: "var(--moco-faint)" }}>{it.label}</span>
</span>
))}
</div>
);
}
components/moco/diagram.css
.moco-dgm {
display: grid;
gap: 1.1rem;
min-width: 460px;
font-size: 13px;
line-height: 1.4;
}
.moco-dgm-row {
display: grid;
grid-template-columns: 108px 1fr;
align-items: center;
gap: 1rem;
}
.moco-dgm-row-label {
color: var(--moco-faint);
font-size: 12px;
text-align: right;
letter-spacing: 0.02em;
}
.moco-dgm-track {
display: flex;
align-items: stretch;
gap: 6px;
flex-wrap: nowrap;
}
.moco-dgm-block {
position: relative;
flex: 1 1 0;
min-width: 0;
border: 1px solid var(--moco-line);
border-radius: 2px;
padding: 0.5rem 0.6rem;
background: var(--moco-bg);
color: var(--moco-body);
text-align: left;
transition:
opacity 200ms var(--moco-ease),
border-color 200ms var(--moco-ease),
background 200ms var(--moco-ease),
color 200ms var(--moco-ease);
}
.moco-dgm-block .moco-b-label {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.moco-dgm-block .moco-b-sub {
display: block;
color: var(--moco-faint);
font-size: 11.5px;
margin-top: 2px;
}
.moco-dgm-block[data-variant="active"] {
background: var(--moco-accent);
border-color: var(--moco-accent);
color: var(--moco-on-accent);
}
/* Sits on the lime fill in both themes, so the dark ink is safe hardcoded. */
.moco-dgm-block[data-variant="active"] .moco-b-sub {
color: rgba(20, 24, 10, 0.72);
}
.moco-dgm-block[data-variant="warn"] {
border-color: var(--moco-warn);
color: var(--moco-warn);
background: var(--moco-warn-wash);
}
.moco-dgm-block[data-variant="warn"] .moco-b-sub {
color: var(--moco-warn);
opacity: 0.72;
}
.moco-dgm-block[data-variant="dashed"] {
border-style: dashed;
background: transparent;
color: var(--moco-faint);
}
.moco-dgm-block[data-variant="ghost"] {
border-color: transparent;
background: transparent;
color: var(--moco-faint);
}
/* Hover: dim siblings. */
.moco-dgm-track:has(.moco-dgm-block:hover) .moco-dgm-block:not(:hover),
.moco-dgm-track:has(.moco-dgm-block:focus-visible) .moco-dgm-block:not(:focus-visible) {
opacity: 0.4;
}
.moco-dgm-tip {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
z-index: 5;
width: max-content;
max-width: 240px;
background: var(--moco-bg);
border: 1px solid var(--moco-line);
border-radius: 2px;
padding: 0.45rem 0.6rem;
font-size: 12px;
color: var(--moco-muted);
opacity: 0;
pointer-events: none;
transition: opacity 150ms var(--moco-ease);
}
.moco-dgm-block:hover .moco-dgm-tip,
.moco-dgm-block:focus-visible .moco-dgm-tip {
opacity: 1;
}
.moco-dgm-diamond {
width: 7px;
height: 7px;
border: 1px solid var(--moco-accent-dk);
transform: rotate(45deg);
display: inline-block;
margin: 0 0.4rem;
flex: none;
}
.moco-dgm-bracket {
border-top: 1px solid var(--moco-line);
border-left: 1px solid var(--moco-line);
border-right: 1px solid var(--moco-line);
height: 8px;
margin-top: 4px;
}
.moco-dgm-bracket-cap {
font-size: 12px;
color: var(--moco-faint);
text-align: center;
margin-top: 6px;
}
.moco-dgm-note {
color: var(--moco-faint);
font-size: 12px;
grid-column: 2;
}
.moco-dgm-legend {
display: flex;
flex-wrap: wrap;
gap: 0.35rem 1.25rem;
font-size: 12px;
color: var(--moco-faint);
border-top: 1px solid var(--moco-line);
padding-top: 0.9rem;
}
.moco-dgm-legend span.moco-sw {
display: inline-block;
width: 9px;
height: 9px;
border: 1px solid currentColor;
margin-right: 0.45rem;
vertical-align: -1px;
}
@media (prefers-reduced-motion: reduce) {
.moco-dgm-block,
.moco-dgm-tip {
transition-duration: 0.001ms;
}
}