API Request
A tiny HTTP client: method picker, URL, headers and body tabs, Send, then status, timing, size and a formatted response.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/api-request.json'use client'
import { useState, type ReactNode } from 'react'
import './api-request.css'
export interface ApiRequestProps {
handler: (req: { method: string; url: string; body: string }) => Promise<{ status: number; body: unknown }>
baseUrl?: string
method?: string
path?: string
fill?: 'dark' | 'light'
className?: string
}
function pretty(v: unknown): ReactNode[] {
return JSON.stringify(v, null, 2).split(/("(?:[^"\\]|\\.)*")(\s*:)?/g).filter((x) => x !== undefined).map((part, i, arr) => {
if (/^".*"$/.test(part)) return <span key={i} className={arr[i + 1]?.trim() === ':' ? 'k' : 's'}>{part}</span>
return part
})
}
/** Minimal HTTP request console. */
export function ApiRequest({ handler, baseUrl = 'https://api.sundae.dev', method: m0 = 'GET', path = '/v1/components?limit=2', fill = 'dark', className }: ApiRequestProps) {
const [method, setMethod] = useState(m0)
const [url, setUrl] = useState(path)
const [tab, setTab] = useState<'params' | 'headers' | 'body'>(m0 === 'GET' ? 'headers' : 'body')
const [body, setBody] = useState('{\n "name": "jelly-switch"\n}')
const [busy, setBusy] = useState(false)
const [res, setRes] = useState<{ status: number; body: unknown; ms: number; size: number } | null>(null)
const send = async () => {
setBusy(true); const t0 = performance.now()
try { const r = await handler({ method, url, body }); const txt = JSON.stringify(r.body); setRes({ ...r, ms: Math.round(performance.now() - t0), size: txt.length }) }
catch (e) { setRes({ status: 0, body: { error: String(e) }, ms: Math.round(performance.now() - t0), size: 0 }) }
setBusy(false)
}
const qs = url.includes('?') ? url.split('?')[1].split('&').map((p) => p.split('=')) : []
return (
<div className={['sd-apr', className].filter(Boolean).join(' ')} data-fill={fill} onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); send() } }}>
<div className="sd-apr__bar">
<select aria-label="Method" value={method} onChange={(e) => setMethod(e.target.value)}>{['GET', 'POST', 'PUT', 'DELETE'].map((x) => <option key={x}>{x}</option>)}</select>
<input aria-label={`URL, base ${baseUrl}`} value={url} onChange={(e) => setUrl(e.target.value)} spellCheck={false} />
<button type="button" disabled={busy} onClick={send}>{busy ? '…' : 'Send'}</button>
</div>
<div className="sd-apr__tabs" role="tablist">{(['params', 'headers', 'body'] as const).map((t) => <button key={t} type="button" role="tab" aria-selected={tab === t} onClick={() => setTab(t)}>{t[0].toUpperCase() + t.slice(1)}{t === 'params' && qs.length ? ` ${qs.length}` : ''}</button>)}</div>
<div className="sd-apr__pane" role="tabpanel">
{tab === 'params' && <div className="sd-apr__hd">{qs.length ? qs.flatMap(([k, v]) => [<span key={k}>{k}</span>, <span key={k + 'v'}>{v ?? ''}</span>]) : <span style={{ gridColumn: 'span 2' }}>No query params</span>}</div>}
{tab === 'headers' && <div className="sd-apr__hd"><span>Authorization</span><span>Bearer sk_test_••••</span><span>Accept</span><span>application/json</span>{method !== 'GET' && <><span>Content-Type</span><span>application/json</span></>}</div>}
{tab === 'body' && <textarea aria-label="Body" value={body} disabled={method === 'GET'} onChange={(e) => setBody(e.target.value)} spellCheck={false} placeholder="GET requests have no body" />}
</div>
<div className="sd-apr__res" aria-live="polite" aria-busy={busy}>
{busy && <div className="sd-apr__ld" />}
{res ? <>
<div className="sd-apr__meta"><em data-err={res.status >= 400 || res.status === 0 || undefined}>{res.status || 'ERR'}</em><span>Time <b>{res.ms} ms</b></span><span>Size <b>{res.size} B</b></span></div>
<pre>{pretty(res.body)}</pre>
</> : !busy && <div className="sd-apr__empty">Send a request to see the response</div>}
</div>
</div>
)
}.sd-apr { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-apr[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-apr :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-apr :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-apr .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-apr { width: min(100%, 360px); grid-template-columns: minmax(0, 1fr); border-radius: 18px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); display: grid; overflow: hidden; }
.sd-apr__bar { display: flex; gap: 6px; padding: 10px; }
.sd-apr__bar select { height: 36px; padding: 0 8px; border: 0; border-radius: 9px; background: var(--soft); color: var(--fg); font: 700 12px ui-monospace, monospace; outline: 0; }
.sd-apr__bar input { flex: 1; min-width: 0; height: 36px; padding: 0 10px; border: 0; border-radius: 9px; background: var(--soft); color: var(--fg); font: 12.5px ui-monospace, monospace; outline: 0; }
.sd-apr__bar input:focus, .sd-apr__bar select:focus { box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-apr__bar button { height: 36px; padding: 0 14px; border-radius: 9px; font-size: 13px; font-weight: 600; background: var(--strong) !important; color: var(--on) !important; }
.sd-apr__bar button:disabled { opacity: 0.5; }
.sd-apr__tabs { display: flex; gap: 14px; padding: 0 12px; border-bottom: 1px solid var(--line); }
.sd-apr__tabs button { padding: 7px 0; font-size: 12.5px; color: var(--mute) !important; box-shadow: inset 0 -2px 0 transparent; }
.sd-apr__tabs button[aria-selected='true'] { color: var(--fg) !important; box-shadow: inset 0 -2px 0 var(--fg); }
.sd-apr__pane { padding: 8px 10px; }
.sd-apr__pane textarea { width: 100%; height: 64px; padding: 8px; border: 0; border-radius: 8px; background: var(--soft); color: var(--fg); font: 12px/1.5 ui-monospace, monospace; resize: none; outline: 0; box-sizing: border-box; }
.sd-apr__hd { display: grid; grid-template-columns: 1fr 1.4fr; gap: 4px; font: 11.5px ui-monospace, monospace; }
.sd-apr__hd span { padding: 5px 8px; border-radius: 6px; background: var(--soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sd-apr__hd span:nth-child(odd) { color: var(--mute); }
.sd-apr__res { border-top: 1px solid var(--line); min-height: 150px; }
.sd-apr__meta { display: flex; gap: 12px; padding: 8px 12px; font-size: 11.5px; color: var(--mute); font-variant-numeric: tabular-nums; }
.sd-apr__meta b { color: var(--fg); }
.sd-apr__meta em { font-style: normal; font-weight: 700; padding: 1px 7px; border-radius: 5px; background: var(--strong); color: var(--on); }
.sd-apr__meta em[data-err] { background: transparent; color: var(--fg); box-shadow: inset 0 0 0 1.5px var(--fg); }
.sd-apr pre { margin: 0 10px 10px; padding: 8px 10px; max-height: 120px; overflow: auto; border-radius: 8px; background: var(--soft); font: 11.5px/1.5 ui-monospace, monospace; }
.sd-apr pre .k { color: var(--fg); font-weight: 600; }
.sd-apr pre .s { color: var(--mute); }
.sd-apr__empty { padding: 40px 12px; text-align: center; color: var(--mute); font-size: 12.5px; }
.sd-apr__ld { height: 2px; background: var(--fg); animation: sd-apr-l 700ms ease-in-out infinite; transform-origin: left; }
@keyframes sd-apr-l { 0% { transform: scaleX(0); } 100% { transform: scaleX(1); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .sd-apr *, .sd-apr *::before, .sd-apr *::after { animation: none !important; transition: none !important; } }Overview
Pick a method, edit the path and send. The response panel shows status, time and size, with the JSON body pretty-printed.
Requests go to a handler you pass in, so the demo is fully offline; wire it to fetch for a real console.
When to use
- API docs try-it panels, developer portals.
When not to use
- Full API debugging: use a dedicated client.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/api-request.json$
npx shadcn@latest add https://sundaeui.dev/r/api-request.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/api-request.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/api-request.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy api-request.tsx and api-request.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { ApiRequest } from '@/components/sundae/api-request'
<ApiRequest />Props
| Prop | Type | Default |
|---|---|---|
handler*Executes the request. | (req: { method: string; url: string; body: string }) => Promise<{ status: number; body: unknown }> | — |
baseUrlShown before the path. | string | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Tabs are a tablist; the response status and time are announced.
- Method select is a native select.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-apr; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
Keyboard
- ⌘ / Ctrl + Enter
- Send
FAQ
- Is API Request free?
- Yes. API Request is free for personal and commercial projects, with full source on this page.
- How do I install API Request?
- Run npx shadcn@latest add https://sundaeui.dev/r/api-request.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does API Request support keyboard and screen readers?
- Tabs are a tablist; the response status and time are announced. Method select is a native select.
- Does API Request respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All forms →OTP Field
Forms · Refined- Pro
Drop Zone
Forms · Refined - Pro
Onboarding Flow
Forms · Refined - Pro
Wheel Picker
Forms · Refined Password Strength
Forms · RefinedRating Scale
Forms · Refined