API Endpoint
A collapsible API reference card — method badge, highlighted path placeholders, grouped parameter tables, expandable response examples, and a Copy-as-cURL button that never lies about failing.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/api-endpoint.jsonPrompt
Build a React + TypeScript + Tailwind "ApiEndpoint" component (lucide-react for
icons, no other runtime dependency).
Contract
- forwardRef<HTMLElement> onto an <article>; the rest of the native props spread
onto it. Nothing is controlled from outside except the data.
- Props: method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; path (a route with
{name} placeholders); summary (one-line human title); description?: ReactNode;
params?: ApiEndpointParam[]; responses?: ApiEndpointResponse[]; auth?:
ApiEndpointAuth; baseUrl = "https://api.example.com"; defaultOpen = false;
className.
- ApiEndpointParam = { name, type, in: "path" | "query" | "header" | "body",
required?, description?: ReactNode, example?: string }. ONE flat array — the
component does the grouping, so the consumer never hand-builds nested objects.
- ApiEndpointResponse = { status: string | number, description, example?: string }.
`example` is an already pretty-printed payload; the component never formats or
re-indents it.
- ApiEndpointAuth = { label, header?, description? }. `header` is a full header
line ("Authorization: Bearer $API_KEY") copied verbatim into the cURL so shell
variables survive; omit it and auth stays out of the command.
- `params` and `responses` default to MODULE-LEVEL empty arrays, never inline []
literals — inline defaults are a new identity every render and would bust the
memoized cURL/grouping on every keystroke of a parent form.
Behavior
- Collapsed by default. The whole title row is one <button type="button"> with
aria-expanded + aria-controls pointing at the panel; the panel is a
grid-rows-[0fr] → [1fr] animation with an overflow-hidden inner wrapper. The
panel keeps `inert` while closed, which is what stops a zero-height-but-present
panel from parking invisible buttons in the tab order; the markup itself still
ships in the server-rendered HTML for crawlers. (inert needs React 19 — React
18 serialises `inert={false}` as the truthy string "false" and would leave the
panel permanently dead; on React 18 swap it for `hidden={!open}` and accept
losing the animation.)
- The Copy-as-cURL button is a SIBLING of the disclosure button, never nested
inside it (a button inside a button is invalid HTML and unreachable by
keyboard).
- buildCurl assembles a real command, it does not template a pretty string:
* `-X <METHOD>` is always emitted, so a `-d` payload can never silently
promote the request to POST.
* path placeholders are replaced by the matching path param's `example`;
with no example the `{name}` placeholder is left in, so the reader sees
exactly what is still theirs to fill.
* query and header params are included when they are `required` OR carry an
`example`; a value-less one renders as a `<name>` / `<type>` placeholder.
* body params become a JSON object: each `example` is JSON.parse'd first, so
"42" / "true" / '{"a":1}' land as real JSON values and anything that fails
to parse is emitted as a JSON string. A body also adds a
Content-Type: application/json header.
* values interpolated into double-quoted shell words escape " \ $ and
backticks; the JSON payload sits in single quotes with ' rewritten as '\''.
`auth.header` alone is inserted verbatim — that is what keeps $API_KEY a
shell variable instead of a literal.
- Clipboard is honest. Missing navigator.clipboard (insecure context, old
browser) or a rejected writeText both land in a "Copy failed" state that
renders the command in a selectable <pre> and moves the selection into it.
The success chip auto-resets after ~2s; the failure block does NOT auto-reset,
because it has to stay on screen long enough to actually be copied by hand.
- Params are grouped in a fixed order — path, query, header, body — and each
non-empty group renders one table. Responses list in the order given; a
response WITH an `example` becomes its own disclosure (aria-expanded +
aria-controls, panel toggled with the `hidden` attribute), a response WITHOUT
one renders as a plain row and never fakes a clickable affordance.
- With no description, auth note, params or responses, the expanded panel says
so in one muted line instead of opening an empty box.
- Cleanup: the copy-reset timeout and the selection requestAnimationFrame are
both cancelled on unmount.
Rendering & styling
- Semantic tokens only: bg-card / text-card-foreground (card), border + bg-muted/40
(tables, example blocks, auth note), text-muted-foreground (labels, secondary
copy), bg-primary/10 + text-primary (GET badge, 2xx status, highlighted path
placeholder), bg-primary + text-primary-foreground (POST badge),
bg-destructive/10 + text-destructive (DELETE badge, 4xx/5xx status, "required"
markers, copy failure), ring-ring for every focus-visible. cn() merges the
consumer className onto the article.
- The five method badges must stay distinguishable on a MONOCHROME palette, so
they differ by treatment, not hue: GET tinted, POST solid, PUT outlined,
PATCH dashed-outline, DELETE destructive. Never colour method or status text
with --chart-* tokens: those are chart fills and score ~1.3:1 as text on a
zero-chroma theme, i.e. invisible in one of the two modes.
- A11y: <article aria-label="METHOD /path">, an <h3> around the disclosure
button, role="region" + aria-labelledby on the panel, one real <table> per
parameter group with an sr-only <caption>, <th scope="col"> headers and
<th scope="row"> on the parameter name, and an aria-live="polite" sr-only
line announcing copy success/failure. Every table sits in an overflow-x-auto
wrapper so narrow cards scroll instead of squeezing columns.
- Motion is decoration: the chevron rotation and the grid-rows transition both
carry motion-reduce:transition-none, and expanding/collapsing works purely on
state, so nothing depends on a transition completing.
Customization levers
- Method set: METHOD_BADGE is a Record keyed by the method union — add HEAD /
OPTIONS by extending both, and keep the "different treatment, not different
hue" rule if the target palette is monochrome.
- Parameter groups: PARAM_GROUPS is an ordered array of { in, label } — reorder
it, rename the headings, or drop a location entirely (e.g. no header table on
a public API).
- Table columns: name / type / description with "required" under the name reads
well down to ~32rem; move `required` into its own column, or add a "default"
column, by editing ParamTable alone.
- cURL dialect: swap buildCurl's output for httpie, fetch(), or a language SDK
snippet — its inputs (method, path, baseUrl, params, auth) already carry
everything those need.
- Density and chrome: p-3/sm:p-4 header + gap-6 body is "docs page"; drop to
p-2 + text-xs for a compact endpoint index. `defaultOpen` per card is what
turns a long index into an expanded single-endpoint page.
- Heading level is hard-coded to <h3> to sit under a page <h2>; change it in one
place if your outline differs.Concepts
- Disclosure card, not an accordion item — the trigger is a fixed shape (method badge + path + summary + auth chip), which is what lets a page stack dozens of endpoints and stay scannable; a generic title/content folder is a different component.
- Monochrome-safe method badges — the default palette here has zero chroma, so hue cannot separate five methods. The badges differ by treatment (tint / solid / outline / dashed outline / destructive), and
--chart-*tokens are deliberately never used on text: as foreground colour they measure ~1.3:1 against this theme. - Generated command, not a pasted string — the cURL is assembled from the same
paramsthat fill the tables, so the tables and the command can never drift.-Xis always explicit so a-dbody cannot silently promote a documented GET into a POST, and bodyexamplestrings areJSON.parsed so numbers and booleans arrive unquoted. - Honest clipboard failure — an insecure context or a rejected
writeTextproduces a visible "Copy failed" state plus a selectable<pre>of the exact command, and unlike the success chip it does not time out. Reporting "Copied" for a write that never happened is the failure mode this exists to prevent. - Collapsed is not removed — the panel animates with
grid-rows, so its content ships in the server-rendered HTML instead of appearing only after hydration;inertwhile closed is the counterweight, keeping a zero-height panel's buttons out of the tab order and out of the accessibility tree (the trade-off: browsers also skip inert text in find-in-page). - Placeholders survive round-trips — a
{param}with no example stays literal in both the highlighted path and the generated URL, so a reader can always tell which segments are still theirs to fill in.
Org Chart
A recursive org/hierarchy chart with pure-CSS connector lines, collapsible subtrees, vertical or horizontal orientation, and full WAI-ARIA tree semantics.
Gantt Chart
A project schedule chart — a sticky task column, an adaptive day/week/month axis, progress-filled bars, milestone diamonds, finish-to-start dependency elbows, collapsible groups and real loading/empty/error states.