Developer

Request line

One line that sums up an HTTP exchange: method, path, status, duration and size, for docs and network logs.

Built on
Container queries
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
2.9 kB
Built from
BadgeSpinner
GET /deploys, 200 OK, 84 milliseconds, 1.2 kilobytes
GET /keys/9, 404 Not Found, 12 milliseconds, 96 bytes
POST /builds, 500 Internal Server Error, 1.9 seconds, 312 bytes
Try its options

Loading its options.

Code
import { HttpStatus } from "@/components/equip/http-status";

export default function Demo() {
  return (
    <div style={{ display: "grid", gap: 4, width: 224 }}>
      <HttpStatus method="GET" path="/deploys" status={200} duration={84} size={1240} />
      <HttpStatus method="GET" path="/keys/9" status={404} duration={12} size={96} />
      <HttpStatus method="POST" path="/builds" status={500} duration={1930} size={312} />
    </div>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:4px;width:224px">
  <div class="eq-http-status">
    <span class="eq-http-status-row">
      <span class="eq-http-status-sr">GET /deploys, 200 OK, 84 milliseconds, 1.2 kilobytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">GET</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/deploys</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="ok" data-variant="soft">
        <span class="eq-badge-label">200<span class="eq-http-status-reason">OK</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">84 ms</span>
      <span class="eq-http-status-size" aria-hidden="true">1.2 kB</span>
    </span>
  </div>
  <div class="eq-http-status">
    <span class="eq-http-status-row">
      <span class="eq-http-status-sr">GET /keys/9, 404 Not Found, 12 milliseconds, 96 bytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">GET</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/keys/9</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="warn" data-variant="soft">
        <span class="eq-badge-label">404<span class="eq-http-status-reason">Not Found</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">12 ms</span>
      <span class="eq-http-status-size" aria-hidden="true">96 B</span>
    </span>
  </div>
  <div class="eq-http-status">
    <span class="eq-http-status-row">
      <span class="eq-http-status-sr">POST /builds, 500 Internal Server Error, 1.9 seconds, 312 bytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">POST</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/builds</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="danger" data-variant="soft">
        <span class="eq-badge-label">500<span class="eq-http-status-reason">Internal Server Error</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">1.9 s</span>
      <span class="eq-http-status-size" aria-hidden="true">312 B</span>
    </span>
  </div>
</div>

Examples

Network panel

Each line is one link whose name is the whole sentence, such as GET /v1/deploys, 200 OK, 84 milliseconds, 1.2 kilobytes.

Code
import { HttpStatus } from "@/components/equip/http-status";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 2, width: "100%", maxWidth: 560 }}>
      <HttpStatus method="GET" path="/v1/projects/web/deploys?limit=20" status={200} duration={84} size={1240} href="#req-1" />
      <HttpStatus method="POST" path="/v1/projects/web/deploys" status={201} duration={212} size={688} href="#req-2" />
      <HttpStatus method="GET" path="/v1/projects/web" status={304} duration={9} size={0} href="#req-3" />
      <HttpStatus method="PATCH" path="/v1/projects/web/env/DATABASE_URL" status={422} duration={41} size={164} href="#req-4" />
      <HttpStatus method="DELETE" path="/v1/projects/web/domains/old.example.com" status={503} duration={30_000} size={58} href="#req-5" />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:2px;width:100%;max-width:560px">
  <div class="eq-http-status">
    <a class="eq-http-status-row" href="#req-1">
      <span class="eq-http-status-sr">GET /v1/projects/web/deploys?limit=20, 200 OK, 84 milliseconds, 1.2 kilobytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">GET</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/v1/projects/web</span>
        <span class="eq-http-status-tail">/deploys?limit=20</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="ok" data-variant="soft">
        <span class="eq-badge-label">200<span class="eq-http-status-reason">OK</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">84 ms</span>
      <span class="eq-http-status-size" aria-hidden="true">1.2 kB</span>
    </a>
  </div>
  <div class="eq-http-status">
    <a class="eq-http-status-row" href="#req-2">
      <span class="eq-http-status-sr">POST /v1/projects/web/deploys, 201 Created, 212 milliseconds, 688 bytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">POST</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/v1/projects/web</span>
        <span class="eq-http-status-tail">/deploys</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="ok" data-variant="soft">
        <span class="eq-badge-label">201<span class="eq-http-status-reason">Created</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">212 ms</span>
      <span class="eq-http-status-size" aria-hidden="true">688 B</span>
    </a>
  </div>
  <div class="eq-http-status">
    <a class="eq-http-status-row" href="#req-3">
      <span class="eq-http-status-sr">GET /v1/projects/web, 304 Not Modified, 9 milliseconds, 0 bytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">GET</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/v1/projects/web</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">304<span class="eq-http-status-reason">Not Modified</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">9 ms</span>
      <span class="eq-http-status-size" aria-hidden="true">0 B</span>
    </a>
  </div>
  <div class="eq-http-status">
    <a class="eq-http-status-row" href="#req-4">
      <span class="eq-http-status-sr">PATCH /v1/projects/web/env/DATABASE_URL, 422 Unprocessable Content, 41 milliseconds, 164 bytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">PATCH</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/v1/projects/web/env</span>
        <span class="eq-http-status-tail">/DATABASE_URL</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="warn" data-variant="soft">
        <span class="eq-badge-label">422<span class="eq-http-status-reason">Unprocessable Content</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">41 ms</span>
      <span class="eq-http-status-size" aria-hidden="true">164 B</span>
    </a>
  </div>
  <div class="eq-http-status">
    <a class="eq-http-status-row" href="#req-5">
      <span class="eq-http-status-sr">DELETE /v1/projects/web/domains/old.example.com, 503 Service Unavailable, 30 seconds, 58 bytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">DELETE</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/v1/projects/web/domains</span>
        <span class="eq-http-status-tail">/old.example.com</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="danger" data-variant="soft">
        <span class="eq-badge-label">503<span class="eq-http-status-reason">Service Unavailable</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">30 s</span>
      <span class="eq-http-status-size" aria-hidden="true">58 B</span>
    </a>
  </div>
</div>

Pending request

Until the response lands there is no status, size or timing, and the line reads as pending.

POST /webhooks/equip/deploy.succeeded, 200 OK, 146 milliseconds, 2 bytes
POST /webhooks/equip/deploy.failed, pending
Code
import { HttpStatus } from "@/components/equip/http-status";

export default function Example() {
  return (
    <div style={{ display: "grid", gap: 2, width: "100%", maxWidth: 480 }}>
      <HttpStatus method="POST" path="/webhooks/equip/deploy.succeeded" status={200} duration={146} size={2} />
      <HttpStatus method="POST" path="/webhooks/equip/deploy.failed" pending />
    </div>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:2px;width:100%;max-width:480px">
  <div class="eq-http-status">
    <span class="eq-http-status-row">
      <span class="eq-http-status-sr">POST /webhooks/equip/deploy.succeeded, 200 OK, 146 milliseconds, 2 bytes</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">POST</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/webhooks/equip</span>
        <span class="eq-http-status-tail">/deploy.succeeded</span>
      </span>
      <span aria-hidden="true" class="eq-badge eq-http-status-code" data-tone="ok" data-variant="soft">
        <span class="eq-badge-label">200<span class="eq-http-status-reason">OK</span></span>
      </span>
      <span class="eq-http-status-duration" aria-hidden="true">146 ms</span>
      <span class="eq-http-status-size" aria-hidden="true">2 B</span>
    </span>
  </div>
  <div class="eq-http-status" data-pending="true">
    <span class="eq-http-status-row">
      <span class="eq-http-status-sr">POST /webhooks/equip/deploy.failed, pending</span>
      <span aria-hidden="true" class="eq-badge eq-http-status-method" data-tone="neutral" data-variant="soft">
        <span class="eq-badge-label">POST</span>
      </span>
      <span class="eq-http-status-path" aria-hidden="true">
        <span class="eq-http-status-head">/webhooks/equip</span>
        <span class="eq-http-status-tail">/deploy.failed</span>
      </span>
      <span class="eq-http-status-pending" aria-hidden="true">
        <span role="status" class="eq-spinner" data-size="s">
          <svg class="eq-spinner-ring" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true">
            <circle class="eq-spinner-track" cx="8" cy="8" r="6.25" />
            <circle class="eq-spinner-arc" cx="8" cy="8" r="6.25" pathLength="100" stroke-dasharray="28 100" />
          </svg>
          <span class="eq-spinner-label">Pending</span>
        </span>
      </span>
    </span>
  </div>
</div>

Props

PropTypeDefaultWhat it does
methodstringnoneGET, POST, PUT, PATCH, DELETE and so on.
pathstringnoneThe request path. Long paths give way in the middle.
statusnumbernoneThe response code. Its class sets the tone: 2xx ok, 3xx neutral, 4xx warn, 5xx danger.
durationnumbernoneRound-trip time in milliseconds.
sizenumbernoneResponse size in bytes, shown in decimal units.
hrefstringnoneMakes the whole line a link.
pendingbooleanfalseIn flight: a spinner stands in for the status.
...restComponentProps<"div">noneEvery native div attribute.

Anatomy

root.eq-http-statusA size container.
row.eq-http-status-rowA link with href, otherwise a span.
method.eq-http-status-methodA badge.
path.eq-http-status-path
head.eq-http-status-headThe part of a long path that gives way.
tail.eq-http-status-tailThe last segment, always shown.
code.eq-http-status-codeA badge toned by status class.
reason.eq-http-status-reasonHidden on narrow lines.
pending.eq-http-status-pending
duration.eq-http-status-duration
size.eq-http-status-sizeHidden on very narrow lines.
sentence.eq-http-status-srThe whole line in words, for screen readers.

Keyboard

EnterFollows the link when href is set.

Notes

  • Screen readers hear one sentence, such as GET /v1/deploys, 200 OK, 84 milliseconds, 1.2 kilobytes; the visual pieces are hidden from them.
  • Status is a code and words as well as a tone, never colour alone.
  • Narrow lines drop the reason words, then the size, but the sentence still says both.
  • The root is a size container with no width of its own, so give it one inside shrink-to-fit parents such as an inline flex row.
  • Numbers are formatted without locale APIs, so the server and the browser always agree.
  • It renders on the server with no client JavaScript; the export is HttpStatus.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/http-status.tsx182 lines
import type { ComponentProps, ReactNode } from "react";
import { Badge } from "./badge";
import { Spinner } from "./spinner";
import "./http-status.css";

export type HttpStatusProps = ComponentProps<"div"> & {
  /** GET, POST, PUT, PATCH, DELETE and so on. */
  method: string;
  /** The request path, such as "/v1/deploys". Long paths give way in the middle. */
  path: string;
  /** The response status code. */
  status?: number;
  /** Round-trip time in milliseconds. */
  duration?: number;
  /** Response size in bytes. */
  size?: number;
  /** Makes the line a link, such as to the request's detail view. */
  href?: string;
  /** The request is in flight: a spinner stands in for the status. */
  pending?: boolean;
};

const REASONS: Record<number, string> = {
  100: "Continue",
  101: "Switching Protocols",
  200: "OK",
  201: "Created",
  202: "Accepted",
  204: "No Content",
  206: "Partial Content",
  301: "Moved Permanently",
  302: "Found",
  303: "See Other",
  304: "Not Modified",
  307: "Temporary Redirect",
  308: "Permanent Redirect",
  400: "Bad Request",
  401: "Unauthorized",
  403: "Forbidden",
  404: "Not Found",
  405: "Method Not Allowed",
  408: "Request Timeout",
  409: "Conflict",
  410: "Gone",
  413: "Content Too Large",
  415: "Unsupported Media Type",
  422: "Unprocessable Content",
  429: "Too Many Requests",
  500: "Internal Server Error",
  501: "Not Implemented",
  502: "Bad Gateway",
  503: "Service Unavailable",
  504: "Gateway Timeout",
};

const CLASSES = ["Informational", "Success", "Redirect", "Client error", "Server error"];
const TONES = ["neutral", "ok", "neutral", "warn", "danger"] as const;

/** Fixed decimals without locale APIs, so server and client agree; drops a trailing ".0". */
function fixed(n: number, digits: number) {
  return n.toFixed(digits).replace(/\.0+$/, "");
}

function plural(n: string, unit: string) {
  return `${n} ${unit}${n === "1" ? "" : "s"}`;
}

function time(ms: number): [string, string] {
  if (ms < 1000) {
    const n = fixed(ms, 0);
    return [`${n} ms`, plural(n, "millisecond")];
  }
  const n = fixed(ms / 1000, ms < 10_000 ? 1 : 0);
  return [`${n} s`, plural(n, "second")];
}

function bytes(b: number): [string, string] {
  const units: [number, string, string][] = [
    [1e9, "GB", "gigabyte"],
    [1e6, "MB", "megabyte"],
    [1e3, "kB", "kilobyte"],
  ];
  for (const [base, short, long] of units) {
    if (b >= base) {
      const v = b / base;
      const n = fixed(v, v < 100 ? 1 : 0);
      return [`${n} ${short}`, plural(n, long)];
    }
  }
  const n = fixed(b, 0);
  return [`${n} B`, plural(n, "byte")];
}

/** The whole path stays in the DOM; only the head shrinks, so the last segment stays readable. */
function middle(path: string): ReactNode {
  if (path.length < 20) return <span className="eq-http-status-head">{path}</span>;
  const cut = path.lastIndexOf("/");
  const tail = cut > 0 && path.length - cut <= 24 ? path.length - cut : 12;
  return (
    <>
      <span className="eq-http-status-head">{path.slice(0, -tail)}</span>
      <span className="eq-http-status-tail">{path.slice(-tail)}</span>
    </>
  );
}

export function HttpStatus({
  method,
  path,
  status,
  duration,
  size,
  href,
  pending = false,
  className,
  ...rest
}: HttpStatusProps) {
  const verb = method.toUpperCase();
  const group = status != null ? Math.min(4, Math.max(0, Math.floor(status / 100) - 1)) : 0;
  const reason = status != null ? (REASONS[status] ?? CLASSES[group]) : "";
  const took = duration != null ? time(duration) : null;
  const weight = size != null ? bytes(size) : null;

  const sentence = [
    `${verb} ${path}`,
    pending ? "pending" : status != null ? `${status} ${reason}` : null,
    took?.[1],
    weight?.[1],
  ]
    .filter(Boolean)
    .join(", ");

  const line = (
    <>
      <span className="eq-http-status-sr">{sentence}</span>
      <Badge className="eq-http-status-method" aria-hidden="true">
        {verb}
      </Badge>
      <span className="eq-http-status-path" aria-hidden="true">
        {middle(path)}
      </span>
      {pending ? (
        <span className="eq-http-status-pending" aria-hidden="true">
          <Spinner size="s" label="Pending" />
        </span>
      ) : (
        status != null && (
          <Badge className="eq-http-status-code" tone={TONES[group]} aria-hidden="true">
            {status}
            <span className="eq-http-status-reason">{reason}</span>
          </Badge>
        )
      )}
      {took && (
        <span className="eq-http-status-duration" aria-hidden="true">
          {took[0]}
        </span>
      )}
      {weight && (
        <span className="eq-http-status-size" aria-hidden="true">
          {weight[0]}
        </span>
      )}
    </>
  );

  return (
    <div
      {...rest}
      className={["eq-http-status", className].filter(Boolean).join(" ")}
      data-pending={pending || undefined}
    >
      {href ? (
        <a className="eq-http-status-row" href={href}>
          {line}
        </a>
      ) : (
        <span className="eq-http-status-row">{line}</span>
      )}
    </div>
  );
}
components/equip/http-status.css136 lines
@layer equip {
  .eq-http-status {
    container-type: inline-size;
    min-width: 0;
    color: var(--eq-ink);
    font: var(--eq-weight) var(--eq-text-s) / 1.3 var(--eq-font);
  }

  .eq-http-status-row {
    box-sizing: border-box;
    display: flex;
    align-items: center;
    gap: var(--eq-space-2);
    min-width: 0;
    min-height: var(--eq-h-s);
    padding-inline: var(--eq-space-2);
    border-radius: var(--eq-radius-s);
    color: inherit;
    text-decoration: none;
  }

  a.eq-http-status-row {
    cursor: pointer;
    transition:
      background-color var(--eq-dur) var(--eq-ease),
      scale var(--eq-dur) var(--eq-ease);
  }

  @media (hover: hover) {
    a.eq-http-status-row:hover {
      background: var(--eq-tint);
    }
  }

  a.eq-http-status-row:active {
    scale: var(--eq-press);
  }

  a.eq-http-status-row:focus-visible {
    outline: 2px solid var(--eq-focus);
    outline-offset: 2px;
  }

  /* Badges in mono, wide enough that stacked lines keep their columns. */
  .eq-http-status-row > :is(.eq-http-status-method, .eq-http-status-code) {
    justify-content: center;
    font-family: var(--eq-font-mono);
    font-variation-settings: var(--eq-font-mono-settings);
  }

  .eq-http-status-row > .eq-http-status-method {
    min-width: calc(4ch + var(--eq-space-3) + 2px);
  }

  .eq-http-status-row > .eq-http-status-code {
    min-width: calc(3ch + var(--eq-space-3) + 2px);
  }

  .eq-http-status-reason {
    margin-inline-start: var(--eq-space-1);
    font-family: var(--eq-font);
    font-weight: var(--eq-weight);
  }

  .eq-http-status-path {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    font-family: var(--eq-font-mono);
    font-variation-settings: var(--eq-font-mono-settings);
  }

  /* The head gives way first; once it is gone the tail shrinks too, ending in an ellipsis. */
  .eq-http-status-head {
    flex: 0 1000 auto;
    min-width: 0;
    overflow: hidden;
    white-space: pre;
    text-overflow: ellipsis;
  }

  .eq-http-status-tail {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: pre;
    text-overflow: ellipsis;
  }

  .eq-http-status-pending {
    display: inline-flex;
    flex: none;
    justify-content: center;
    min-width: calc(3ch + var(--eq-space-3) + 2px);
    color: var(--eq-ink-2);
    font-family: var(--eq-font-mono);
    font-size: var(--eq-text-xs);
  }

  .eq-http-status-duration,
  .eq-http-status-size {
    flex: none;
    min-width: 6ch;
    color: var(--eq-ink-2);
    font-size: var(--eq-text-xs);
    font-variant-numeric: tabular-nums;
    text-align: end;
    white-space: nowrap;
  }

  /* Narrow lines keep the code and the timing; the words and the size are still read aloud. */
  @container (max-width: 360px) {
    .eq-http-status-reason {
      display: none;
    }
  }

  @container (max-width: 260px) {
    .eq-http-status-size {
      display: none;
    }
  }

  /* Read by screen readers, never seen. */
  .eq-http-status-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index