Skip to content
RU
Scale
Density
Theme
Accent

Data display

Code and copyable value

JS Copying to the clipboard is done by `instrument.js`

A block of code and a single-line value one wants to copy: a path, a hash, an identifier.

The page is about both: the first is shown, the second is taken.

The component
go -C tools run ./cmd/contrast
a4f7c2e91b0d5537
Markup
<div class="inst-code">go -C tools run ./cmd/contrast
  <button class="inst-copy" type="button" aria-label="Copy the command">
    <svg class="inst-icon" aria-hidden="true"><use href="#i-copy"/></svg>
  </button>
</div>
<span class="inst-copyable">a4f7c2e91b0d5537
  <button class="inst-copy" type="button" aria-label="Copy a4f7c2e91b0d5537">
    <svg class="inst-icon" aria-hidden="true"><use href="#i-copy"/></svg>
  </button>
</span>

Contract

What Required Why
tabindex="0" and a named role="region" yes, if there is nothing focusable inside The block scrolls horizontally, and the scrolling has to be reachable from the keyboard
An aria-label with the value on the copy button yes A row of "Copy" buttons is indistinguishable by ear: "Copy a4f7c2e"
Nothing for speaking the copy — instrument.js starts a shared live region [data-inst-live] itself and speaks into it in words. There is nothing for the application to do here

There is no line wrapping in a block of code: a wrap in a command changes its meaning. Hence the horizontal scrolling, and scrollbar-gutter is reserved so that the appearance of the bar does not shift the code by a pixel on every update.

Accessibility

Scrolling The block scrolls horizontally, so it has to be reachable from the keyboard: tabindex="0" and a named role="region" if there is nothing focusable inside
scrollbar-gutter Reserved so that the appearance of the bar does not shift the code by a pixel on every update
The name of the copy button An aria-label with the value: "Copy a4f7c2e". A row of "Copy" buttons is indistinguishable by ear
The result of a copy instrument.js changes the look of the button and says it in words: a change of glyph is an event with no content for a screen reader. The kit has one live region per document, started at the first message and reused
Line wrapping There is none in a block of code: a wrap in a command changes its meaning. Hence the horizontal scrolling
Type size --text-xs — 12.5px. The size does not go below --text-2xs

Variants

A copyable value

A single-line value with a button — a hash, an identifier, a path.

Example
a4f7c2e
Markup
<span class="inst-copyable">a4f7c2e
  <button class="inst-copy" type="button" aria-label="Copy a4f7c2e">
    <svg class="inst-icon" aria-hidden="true"><use href="#i-copy"/></svg>
  </button>
</span>

The button stands in the flow, with a gap, and is always visible. An absolute in the corner appearing on hover is a trick of a block of code: there the corner is free and there is plenty of text. A single-line value has no corner, and the icon lay over the last characters — and on touch it was not there at all, because on touch there is no hover.

The tap area of the button grows to the norm by an invisible ::before — the same trick as on a checkbox: the area grows rather than the glyph, and the line stays the same height.

A copyable value in a row

The commonest place for one, and the one that looks least like a block of code: a column of identifiers in a table, a machine value in a list of properties. The markup is the same inst-copyable — a table changes nothing about it.

Example
RunIdentifierState
nightly-4127 2f8a1c04-9b7e-4d31-a5f0-c6e2 running
audio-pass 7c31b9de-2a44-4e08-8f1b-30d7 fell
Markup
<table class="inst-table">
  <thead><tr><th>Run</th><th>Identifier</th><th>State</th></tr></thead>
  <tbody>
    <tr><td>nightly-4127</td>
      <td><span class="inst-copyable"><code>2f8a1c04-9b7e-4d31-a5f0-c6e2</code>
        <button class="inst-copy" type="button" aria-label="Copy 2f8a1c04-9b7e-4d31-a5f0-c6e2">
          <svg class="inst-icon" aria-hidden="true"><use href="#i-copy"/></svg>
        </button></span></td>
      <td><span class="inst-badge" data-tone="running"><span class="inst-dot"></span>running</span></td></tr>
    <tr><td>audio-pass</td>
      <td><span class="inst-copyable"><code>7c31b9de-2a44-4e08-8f1b-30d7</code>
        <button class="inst-copy" type="button" aria-label="Copy 7c31b9de-2a44-4e08-8f1b-30d7">
          <svg class="inst-icon" aria-hidden="true"><use href="#i-copy"/></svg>
        </button></span></td>
      <td><span class="inst-badge" data-tone="error"><span class="inst-dot"></span>fell</span></td></tr>
  </tbody>
</table>

Three things about a row that are easy to get wrong, and the reference got two of them wrong itself before this section was written:

What Why
The button is always visible rather than shown on hover over the row A row of identifiers on a touchscreen has no hover, so a button revealed by one does not exist there. The reference used to hide it that way in its own API tables
The colour is --text-muted rather than --text-faint Faint is drawn at a threshold of 3:1 and is meant for lines and dividers. A control is read, so it is due the 4.5:1 of muted
The aria-label carries the value A column of ten "Copy" buttons is one word repeated ten times by ear. "Copy 2f8a1c04" says which row it is

The size of the glyph is not written here and must not be: .inst-copy is --size-icon-sm wherever it stands, because a glyph beside a value serves the value. Call sites that chose for themselves chose differently — that is exactly how one kit came to have an 18px copy button and a 10px one on the same page.

A listing with line numbers

Example
41const size = 256;
42let seed = Date.now();
43return generate(size, seed);
Markup
<div class="inst-code inst-code--numbered">
  <div class="inst-code-line"><span class="inst-code-num">41</span><span class="inst-code-src">const size = 256;</span></div>
  <div class="inst-code-line"><span class="inst-code-num">42</span><span class="inst-code-src">let seed = Date.now();</span></div>
  <div class="inst-code-line"><span class="inst-code-num">43</span><span class="inst-code-src">return generate(size, seed);</span></div>
</div>

A number is not selected by the mouse — user-select: none: what gets copied is the code rather than the page. The column of numbers is exactly 5ch, and that is not a margin by eye: a five-digit line number occurs in real files, and a column growing from its content would shift the code on every load.

inst-code inst-copyable
How many lines Any number One
Scrolling Horizontal None
The button No Yes
An example A command, a config A hash, a path, an identifier

JS

Include the module once per page — there is no need to initialise the components one by one, instrument.js works by delegation and sees nodes that arrived later.

<script type="module" src="instrument.js"></script>

What instrument.js does

The examples on this page are live: hover over the block of code and press the copy button — the glyph turns into a tick.

Copying entire: the text into the clipboard, a coloured response on the button and the result announced aloud. The response is obligatory — a clipboard shows nothing, and without it a person presses the button a second time.

The source is the text of the nearest .inst-code or .inst-copyable without the button itself. textContent is read rather than innerText: the second returns an empty string on a collapsed block, and the copy would go nowhere in silence.

Options

Attribute What it does
data-copy Copy this value rather than the text of the block. Wanted when an abbreviation is shown and the full thing has to be copied
data-copied-label A phrase of your own for a screen reader instead of «Copied»
data-failed-label The same for a failure. «Could not copy» by default
<span class="inst-copyable">a4f7c2e
  <button class="inst-copy" type="button" data-copy="a4f7c2e91b0d5537"
          aria-label="Copy the full hash"></button>
</span>

Events

inst:copy bubbles with { text } and is cancellable: preventDefault() means "I copy it myself", and instrument.js does not touch the clipboard.

document.addEventListener('inst:copy', (e) => {
  analytics.track('copy', { length: e.detail.text.length });
});

What instrument.js does not do

Syntax highlighting. It calls for parsing a language, and that is the work of the application.

API

Classes
NameWhat it does
class
inst-codeA block of code. It scrolls horizontally
inst-code-lineA line of a listing. Inside --numbered only
inst-code-numThe number. --text-muted, because it gets read. Not selected by the mouse
inst-code-srcThe code itself. The one place overflow-wrap: anywhere lives in
inst-copyableA single-line value with a button
inst-copyThe copy button. The work is done by [instrument.js](../../foundations/behavior.md). Inside .inst-code it takes the far top corner by itself
modifier
inst-code--numberedA listing with a column of line numbers
Attributes
NameValueWhat it does
attribute
data-copy—Copy this value rather than the text of the block
data-copied-label—A phrase of your own for a screen reader instead of «Copied»
data-failed-label—The same for a failure. «Could not copy» by default
data-copiedtrue · falseThe response of the button. Set and removed by instrument.js
Tokens
NameValueWhere from
token
--surface-sunkenlight-dark(var(--n-2), var(--n-14))base +1: theme
--border-subtlelight-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06))one across every cell
--hairline1pxbase +1: screen
--radius-md8pxbase +4: scale
--space-48pxone across every cell
--pad-panelvar(--space-5)base +14: density · scale
--text-xs0.78125rembase +4: scale
--font-monoui-monospace, " , " , " , Consolas, " , monospaceone across every cell

Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument