Developer
Key value
A description list for facts about one thing, such as IDs, regions, versions and URLs, with optional copy.
- Built on
<dl>CSS subgridAsync Clipboard API- Client JavaScript
- Yes, for state
- Works without React
- React only (needs JavaScript)
- Source, gzipped
- 3.0 kB
- Dependencies
- React
- ID
- dpl_7f3a9c2e81b4d05a
- Region
- eu-west-1
- Commit
- 7f3a9c2e81b4d05a6c1e
Try its options
Loading its options.
Code
import { KeyValue, KeyValueItem } from "@/components/equip/key-value";
export default function Demo() {
return (
<KeyValue label="Deployment" dense style={{ width: 224 }}>
<KeyValueItem term="ID" mono copy>
dpl_7f3a9c2e81b4d05a
</KeyValueItem>
<KeyValueItem term="Region">eu-west-1</KeyValueItem>
<KeyValueItem term="Commit" mono copy>
7f3a9c2e81b4d05a6c1e
</KeyValueItem>
<KeyValueItem term="URL" href="https://web-7f3a9c.example.com" copy>
web-7f3a9c.example.com
</KeyValueItem>
</KeyValue>
);
}Your versionchange its options in Look, This part
Examples
Stacked and grid
Grid fits as many columns as the width allows, which suits a summary above a detail page.
- Engine
- Postgres 16.4
- Region
- eu-central-1
- Storage
- 48.2 GB of 100 GB
- Replicas
- 2 read replicas
- Host
- db-prod-7f3a.eu-central-1.equip.internal
- Port
- 5432
Code
import { KeyValue, KeyValueItem } from "@/components/equip/key-value";
export default function Example() {
return (
<div style={{ display: "grid", gap: 24, maxWidth: 560 }}>
<KeyValue label="Database" layout="grid">
<KeyValueItem term="Engine">Postgres 16.4</KeyValueItem>
<KeyValueItem term="Region">eu-central-1</KeyValueItem>
<KeyValueItem term="Storage">48.2 GB of 100 GB</KeyValueItem>
<KeyValueItem term="Replicas">2 read replicas</KeyValueItem>
</KeyValue>
<KeyValue label="Connection" layout="stacked" style={{ maxWidth: 320 }}>
<KeyValueItem term="Host" mono copy>
db-prod-7f3a.eu-central-1.equip.internal
</KeyValueItem>
<KeyValueItem term="Port" mono>
5432
</KeyValueItem>
</KeyValue>
</div>
);
}Middle truncation and copy
Long values give way in the middle so both ends stay readable; the full value is in the tooltip and is what gets copied.
- Key ID
- key_01HZX4T8M3QK7N2R9VBW5YCDE6
- Deploy
- pnpm run deploy --prod
Code
import { KeyValue, KeyValueItem } from "@/components/equip/key-value";
export default function Example() {
return (
<KeyValue label="API key" style={{ maxWidth: 340 }}>
<KeyValueItem term="Key ID" mono copy>
key_01HZX4T8M3QK7N2R9VBW5YCDE6
</KeyValueItem>
<KeyValueItem term="Webhook" href="https://hooks.example.com/deploys/production" copy>
https://hooks.example.com/deploys/production
</KeyValueItem>
<KeyValueItem term="Deploy" mono copy="pnpm run deploy --prod">
pnpm run deploy --prod
</KeyValueItem>
</KeyValue>
);
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
label | string | none | KeyValue: names the list for assistive tech. |
layout | "rows" | "stacked" | "grid" | "rows" | KeyValue: term beside value, term above value, or pairs in as many columns as fit. |
dense | boolean | false | KeyValue: tighter spacing for side panels. |
term | ReactNode | none | KeyValueItem: the name of the fact. |
copy | string | true | none | KeyValueItem: shows a copy button; true copies the full value, a string copies that text. |
mono | boolean | false | KeyValueItem: sets the value in the monospace face. |
href | string | none | KeyValueItem: makes the value a link. |
children | ReactNode | none | KeyValueItem: the value. A string truncates in the middle. |
...rest | ComponentProps<"dl"> / ComponentProps<"div"> | none | Native attributes for the list and for each pair. |
Anatomy
| root | .eq-key-value | The dl. |
|---|---|---|
| item | .eq-key-value-item | A div grouping one dt and dd. |
| term | .eq-key-value-term | |
| value | .eq-key-value-value | The dd. |
| text | .eq-key-value-text | A span, or a link with href. |
| head | .eq-key-value-head | The part of a long value that gives way. |
| tail | .eq-key-value-tail | The end of a long value, always shown. |
| copy | .eq-key-value-copy | |
| status | .eq-key-value-sr | Announces Copied. |
Keyboard
| Tab | Moves to each link and copy button. |
|---|---|
| EnterSpace | Copies that value; Enter follows a link. |
Notes
- It is a real dl with dt and dd, so screen readers announce terms and values as pairs.
- Long string values truncate in the middle; the full value is in the DOM, in the tooltip and in what is copied.
- Only string or number children truncate in the middle; other nodes render as given.
- The file is a client component only for the copy buttons; everything else renders on the server.
- The rows layout aligns terms with CSS subgrid; without it each pair stacks, term above value. Copy falls back to a temporary textarea and execCommand where the async clipboard is unavailable.
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/key-value.tsx154 lines
"use client";
import { useEffect, useRef, useState } from "react";
import type { ComponentProps, ReactNode } from "react";
import "./key-value.css";
export type KeyValueProps = ComponentProps<"dl"> & {
/** Names the list for assistive tech, such as "Deployment details". */
label?: string;
/** Term beside value, term above value, or stacked pairs in columns. */
layout?: "rows" | "stacked" | "grid";
/** Tighter spacing for side panels and inspectors. */
dense?: boolean;
};
export type KeyValueItemProps = ComponentProps<"div"> & {
term: ReactNode;
/** Shows a copy button: `true` copies the value as shown in full, a string copies that text instead. */
copy?: string | true;
/** Sets the value in the monospace face, for IDs, hashes and paths. */
mono?: boolean;
/** Makes the value a link. */
href?: string;
};
const COPIED_MS = 1600;
type CopyState = "idle" | "copied" | "failed";
/** Last resort for browsers without the async clipboard: a throwaway textarea inside the part. */
function legacyCopy(text: string, host: HTMLElement) {
const active = document.activeElement;
const area = document.createElement("textarea");
area.value = text;
area.setAttribute("readonly", "");
area.setAttribute("aria-hidden", "true");
area.style.position = "fixed";
area.style.inset = "0 auto auto 0";
area.style.opacity = "0";
area.style.pointerEvents = "none";
host.appendChild(area);
area.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch {
ok = false;
}
host.removeChild(area);
if (active instanceof HTMLElement) active.focus();
return ok;
}
/** The whole text stays in the DOM; only the head shrinks, so the end of an ID or URL stays readable. */
function middle(text: string): ReactNode {
if (text.length < 16) return <span className="eq-key-value-head">{text}</span>;
const tail = Math.min(12, Math.floor(text.length / 3));
return (
<>
<span className="eq-key-value-head">{text.slice(0, -tail)}</span>
<span className="eq-key-value-tail">{text.slice(-tail)}</span>
</>
);
}
export function KeyValue({ label, layout = "rows", dense = false, className, ...rest }: KeyValueProps) {
return (
<dl
aria-label={label}
{...rest}
className={["eq-key-value", className].filter(Boolean).join(" ")}
data-layout={layout}
data-dense={dense || undefined}
/>
);
}
export function KeyValueItem({ term, copy, mono = false, href, className, children, ...rest }: KeyValueItemProps) {
const rootRef = useRef<HTMLDivElement>(null);
const valueRef = useRef<HTMLElement>(null);
const [copied, setCopied] = useState<CopyState>("idle");
const full = typeof children === "string" || typeof children === "number" ? String(children) : null;
const content = full != null ? middle(full) : children;
useEffect(() => {
if (copied === "idle") return;
const t = window.setTimeout(() => setCopied("idle"), COPIED_MS);
return () => window.clearTimeout(t);
}, [copied]);
async function handleCopy() {
const text = typeof copy === "string" ? copy : (full ?? valueRef.current?.textContent ?? "");
let ok = false;
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
ok = true;
}
} catch {
ok = false;
}
if (!ok && rootRef.current) ok = legacyCopy(text, rootRef.current);
setCopied(ok ? "copied" : "failed");
}
const shared = {
className: "eq-key-value-text",
"data-mono": mono || undefined,
title: full ?? undefined,
};
return (
<div {...rest} ref={rootRef} className={["eq-key-value-item", className].filter(Boolean).join(" ")}>
<dt className="eq-key-value-term">{term}</dt>
<dd className="eq-key-value-value">
{href ? (
<a {...shared} ref={(node) => { valueRef.current = node; }} href={href}>
{content}
</a>
) : (
<span {...shared} ref={(node) => { valueRef.current = node; }}>
{content}
</span>
)}
{copy != null && (
<button
type="button"
className="eq-key-value-copy"
data-state={copied}
aria-label={typeof term === "string" ? `Copy ${term.toLowerCase()}` : "Copy value"}
title={copied === "copied" ? "Copied" : copied === "failed" ? "Copy failed" : "Copy"}
onClick={handleCopy}
>
{copied === "copied" ? (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M3.5 8.5l3 3 6-7" pathLength={1} />
</svg>
) : (
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="5.5" y="5.5" width="8" height="8" rx="1.5" />
<path d="M10.5 3.5v-.5A1.5 1.5 0 0 0 9 1.5H4A1.5 1.5 0 0 0 2.5 3v5A1.5 1.5 0 0 0 4 9.5h.5" />
</svg>
)}
</button>
)}
{copy != null && (
<span className="eq-key-value-sr" role="status">
{copied === "copied" ? "Copied" : copied === "failed" ? "Copy failed" : ""}
</span>
)}
</dd>
</div>
);
}components/equip/key-value.css184 lines
@layer equip {
.eq-key-value {
--_pad: var(--eq-space-2);
display: grid;
grid-template-columns: fit-content(40%) minmax(0, 1fr);
column-gap: var(--eq-space-4);
min-width: 0;
margin: 0;
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-s) / 1.45 var(--eq-font);
}
.eq-key-value[data-dense] {
--_pad: var(--eq-space-1);
}
.eq-key-value-item {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
align-items: baseline;
min-width: 0;
padding-block: var(--_pad);
border-top: 1px solid var(--eq-line);
}
.eq-key-value-item:first-child {
border-top: 0;
}
.eq-key-value[data-layout="stacked"] {
grid-template-columns: minmax(0, 1fr);
}
.eq-key-value:is([data-layout="stacked"], [data-layout="grid"]) .eq-key-value-item {
grid-template-columns: minmax(0, 1fr);
row-gap: 2px;
}
.eq-key-value[data-layout="grid"] {
grid-template-columns: repeat(auto-fill, minmax(min(100%, 160px), 1fr));
gap: var(--eq-space-3) var(--eq-space-5);
}
.eq-key-value[data-layout="grid"] .eq-key-value-item {
grid-column: auto;
padding-block: 0;
border-top: 0;
}
.eq-key-value-term {
min-width: 0;
color: var(--eq-ink-2);
overflow-wrap: anywhere;
}
.eq-key-value-value {
display: flex;
align-items: baseline;
gap: var(--eq-space-1);
min-width: 0;
margin: 0;
}
.eq-key-value-text {
display: flex;
flex: 0 1 auto;
min-width: 0;
color: var(--eq-ink);
}
.eq-key-value-text[data-mono] {
font-family: var(--eq-font-mono);
font-variation-settings: var(--eq-font-mono-settings);
font-size: var(--eq-text-xs);
}
.eq-key-value-head {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
white-space: pre;
text-overflow: ellipsis;
}
.eq-key-value-tail {
flex: none;
white-space: pre;
}
a.eq-key-value-text {
border-radius: 2px;
text-decoration: underline;
text-decoration-color: var(--eq-line-strong);
text-decoration-thickness: 1px;
text-underline-offset: 3px;
transition: text-decoration-color var(--eq-dur) var(--eq-ease);
}
@media (hover: hover) {
a.eq-key-value-text:hover {
text-decoration-color: currentColor;
}
.eq-key-value-copy:hover {
background: var(--eq-tint);
color: var(--eq-ink);
}
}
a.eq-key-value-text:focus-visible {
outline: 2px solid var(--eq-focus);
outline-offset: 2px;
}
.eq-key-value-copy {
display: grid;
flex: none;
place-items: center;
align-self: center;
width: 24px;
height: 24px;
margin-block: -4px;
padding: 0;
border: 0;
border-radius: var(--eq-radius-s);
background: transparent;
color: var(--eq-ink-3);
cursor: pointer;
transition:
background-color var(--eq-dur) var(--eq-ease),
color var(--eq-dur) var(--eq-ease),
scale var(--eq-dur) var(--eq-ease);
}
.eq-key-value-copy > svg {
width: 14px;
height: 14px;
}
.eq-key-value-copy:active {
scale: var(--eq-press);
}
.eq-key-value-copy:focus-visible {
outline: 2px solid var(--eq-focus);
outline-offset: 2px;
}
.eq-key-value-copy[data-state="copied"] {
color: var(--eq-ok);
}
.eq-key-value-copy[data-state="failed"] {
color: var(--eq-danger);
}
/* Copied: the tick draws itself in. */
.eq-key-value-copy[data-state="copied"] path {
stroke-dasharray: 1;
stroke-dashoffset: 0;
transition: stroke-dashoffset var(--eq-dur-move) var(--eq-ease);
}
@starting-style {
.eq-key-value-copy[data-state="copied"] path {
stroke-dashoffset: 1;
}
}
/* Read by screen readers, never seen. */
.eq-key-value-sr {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
}