# Code block

Code in a well, colored by kind and numbered by line, with a top bar for a file name or language tabs and small icon actions such as copy.

Block: `smbk-code`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `bare` | on / off | off | No top bar: just the well. |

Parts: `smbk-code__bar`, `smbk-code__label`, `smbk-code__icon`, `smbk-code__tabs`, `smbk-code__actions`, `smbk-code__action`, `smbk-code__body`, `smbk-code__numbers`, `smbk-code__code`, `smbk-code__status`, `smbk-code__keyword`, `smbk-code__declaration`, `smbk-code__string`, `smbk-code__number`, `smbk-code__function`, `smbk-code__type`, `smbk-code__tag`, `smbk-code__property`, `smbk-code__comment`, `smbk-code__punctuation`

## Markup

```html
<div class="smbk-code">
  <div class="smbk-code__bar">
    <span
      class="smbk-label smbk-label--tone-subtle smbk-label--type-default smbk-code__label"
      id="code-block-1-label"
    >
      <i class="ph ph-file-css smbk-icon smbk-icon--size-md smbk-code__icon" aria-hidden="true"></i>
      <span class="smbk-label__text">pricing.css</span>
    </span>
    <div class="smbk-code__actions">
      <button type="button" class="smbk-code__action" aria-label="Copy">
        <i class="ph ph-copy smbk-icon smbk-icon--size-md" aria-hidden="true"></i>
      </button>
    </div>
  </div>
  <div class="smbk-code__body" id="code-block-1" data-scrollbar="" dir="ltr">
    <span class="smbk-code__numbers" aria-hidden="true">1
2
3
4
5</span>
    <pre
      class="smbk-code__code"
      dir="ltr"
    ><code><span class="smbk-code__tag">.pricing-card</span> <span class="smbk-code__punctuation">{</span>
  <span class="smbk-code__property">padding</span><span class="smbk-code__punctuation">:</span> <span class="smbk-code__function">var</span><span class="smbk-code__punctuation">(</span><span class="smbk-code__property">--smbk-space-inset-lg</span><span class="smbk-code__punctuation">);</span>
  <span class="smbk-code__property">border-radius</span><span class="smbk-code__punctuation">:</span> <span class="smbk-code__function">var</span><span class="smbk-code__punctuation">(</span><span class="smbk-code__property">--smbk-radius-container-md</span><span class="smbk-code__punctuation">);</span>
  <span class="smbk-code__property">background</span><span class="smbk-code__punctuation">:</span> <span class="smbk-code__function">var</span><span class="smbk-code__punctuation">(</span><span class="smbk-code__property">--smbk-color-surface-raised</span><span class="smbk-code__punctuation">);</span>
<span class="smbk-code__punctuation">}</span></code></pre>
  </div>
  <span role="status" class="smbk-code__status"></span>
</div>
```

## React

```tsx
import { CodeBlock } from './design-system/components/code-block/code-block.js';

<CodeBlock label="pricing.css" language="css" actions={[{ icon: "Copy", label: "Copy", onClick: () => {} }]}>
  {`.pricing-card {
  padding: var(--smbk-space-inset-lg);
  border-radius: var(--smbk-radius-container-md);
  background: var(--smbk-color-surface-raised);
}`}
</CodeBlock>
```

## Stylesheet

```css
/* smbk-code — Code block. Figma "CodeBlock" 4255:6452 and "CodeBlockAction" 4255:6264.
 * The code is set in Code MD, the mono face, always left to right, beside its line numbers, each kind of code in
 * its code color. The bar names the code with a file icon and a subtle label, the label component's own, or
 * switches it with tabs. Its actions are ghost icon buttons in a 24 px box. */

.smbk-code {
  box-sizing: border-box;
  /* Holds the status line, which is out of sight, inside the block rather than wherever the page would put it. */
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  overflow: hidden;
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
  border-radius: var(--smbk-radius-container-sm);
  color: var(--smbk-color-text-default);
}

.smbk-code__bar {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--smbk-space-inline-lg);
  /* The edge is drawn inside the bar, as Figma counts it: the padding under the row gives its width back. */
  padding: var(--smbk-space-inset-md);
  padding-bottom: calc(var(--smbk-space-inset-md) - var(--smbk-border-width-hairline));
  border-bottom: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
  background: var(--smbk-color-surface-default);
}

/* The label sits on the label component's subtle look (smbk-label smbk-label--tone-subtle), which sets its text and
   the gap after the icon. */
.smbk-code__label {
  padding: 0 var(--smbk-space-inset-xs);
}

.smbk-code__icon {
  color: var(--smbk-color-text-disabled);
  font-size: var(--smbk-size-icon-sm);
}

.smbk-code__tabs {
  display: inline-flex;
}

/* With tabs the body is their panel, which Tab reaches: its ring is drawn inside its edge, because the block clips what runs past it. */
.smbk-code__body:focus-visible {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: calc(-1 * var(--smbk-focus-ring-width));
}

.smbk-code__actions {
  display: inline-flex;
  align-items: center;
  gap: var(--smbk-space-inline-xs);
}

.smbk-code__action {
  box-sizing: border-box;
  display: inline-grid;
  place-content: center;
  place-items: center;
  gap: var(--smbk-space-inline-xs);
  height: var(--smbk-size-control-xs);
  padding: var(--smbk-space-inset-xs);
  border: 0;
  border-radius: var(--smbk-radius-control-sm);
  background: none;
  color: var(--smbk-color-text-subtle);
  font-size: var(--smbk-size-icon-sm);
  cursor: pointer;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
}

/* Hovered and pressed, the ghost button's fills. The focus ring alone shows focus: Figma's raised fill is the
   bar's own. */
@media (hover: hover) {
  .smbk-code__action:hover {
    background: var(--smbk-color-action-ghost-background-hover);
    color: var(--smbk-color-text-default);
  }
}

.smbk-code__action:active {
  background: var(--smbk-color-action-ghost-background-pressed);
  color: var(--smbk-color-text-default);
}

.smbk-code__action:focus-visible {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: var(--smbk-focus-ring-offset);
}

.smbk-code__action:disabled {
  background: none;
  color: var(--smbk-color-text-disabled);
  cursor: not-allowed;
}

/* The action's icon has one cell: an icon the page swaps for another (Copy, then Check once copied) shares it with
   the one it replaces while the two cross-fade (motion, below). */
.smbk-code__action > * {
  grid-area: 1 / 1;
}

.smbk-code__body {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--smbk-space-inline-lg);
  padding: var(--smbk-space-inset-xl);
  background: var(--smbk-color-surface-raised);
  overflow: auto;
  /* The system's scrollbar: thin, in the subtle border color, on a clear track. */
  scrollbar-width: thin;
  scrollbar-color: var(--smbk-color-border-subtle) transparent;
}

.smbk-code__numbers,
.smbk-code__code {
  display: block;
  margin: 0;
  font-family: var(--smbk-typography-code-md-font-family);
  font-size: var(--smbk-typography-code-md-font-size);
  font-weight: var(--smbk-typography-code-md-font-weight);
  line-height: var(--smbk-typography-code-md-line-height);
  letter-spacing: var(--smbk-typography-code-md-letter-spacing);
  text-transform: var(--smbk-typography-code-md-text-transform);
  white-space: pre;
  tab-size: 2;
}

/* The line numbers: a column the reader can point by, quieter than the code, left out of a selection. */
.smbk-code__numbers {
  flex: none;
  color: var(--smbk-color-text-disabled);
  text-align: end;
  -webkit-user-select: none;
  user-select: none;
}

/* ---- each kind of code in its code color ---- */

.smbk-code__keyword { color: var(--smbk-color-code-keyword); }
.smbk-code__declaration { color: var(--smbk-color-code-declaration); }
.smbk-code__string { color: var(--smbk-color-code-string); }
.smbk-code__number { color: var(--smbk-color-code-number); }
.smbk-code__function { color: var(--smbk-color-code-function); }
.smbk-code__type { color: var(--smbk-color-code-type); }
.smbk-code__tag { color: var(--smbk-color-code-tag); }
.smbk-code__property { color: var(--smbk-color-code-property); }
.smbk-code__comment { color: var(--smbk-color-code-comment); }
.smbk-code__punctuation { color: var(--smbk-color-code-punctuation); }

/* The status line: what the last action did, for a screen reader only. Out of sight but in the page,
   so what is put in it is read aloud. */
/* .smbk-code__status: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */

/* ---- motion: an action whose icon the page swaps (Copy, then Check once copied) cross-fades the two in their one
   cell. The leaving icon fades out with the feedback purpose, shrinking and blurring as it goes; the arriving one
   comes back from that look, slower, with the enter purpose, so it lands after the other has gone. Not the exit
   purpose for the leaving one: its easing holds an icon almost whole until its last moment, so the two would show
   on top of each other. The icon an action starts with simply shows. Under reduced motion the two only cross-fade. ---- */

.smbk-code__action > [data-swap] {
  transition-property: opacity, scale, filter;
  transition-duration: var(--smbk-motion-enter-duration);
  transition-timing-function: var(--smbk-motion-enter-easing);
}

.smbk-code__action > [data-swap="out"] {
  opacity: 0;
  transition-duration: var(--smbk-motion-feedback-duration);
  transition-timing-function: var(--smbk-motion-feedback-easing);
}

@starting-style {
  .smbk-code__action > [data-swap="in"] {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .smbk-code__action > [data-swap="out"] {
    scale: 0.6;
    filter: blur(var(--smbk-blur-4));
  }

  @starting-style {
    .smbk-code__action > [data-swap="in"] {
      scale: 0.6;
      filter: blur(var(--smbk-blur-4));
    }
  }
}

/* ---- touch: each action takes an invisible press area ----
   On a touch screen an invisible area, centred on each action, takes the press out to the comfortable target in each
   direction it falls short of it, over the code under the bar, and between two actions only halfway into their gap;
   the block cuts it off at its edge. The actions, their focus ring and their places stay as drawn. */

@media (pointer: coarse) {
  .smbk-code__action {
    position: relative;
  }

  .smbk-code__action::after {
    z-index: 1;
    content: '';
    position: absolute;
    inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
  }

  .smbk-code__action ~ .smbk-code__action::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-xs) / -2);
  }

  .smbk-code__action:has(~ .smbk-code__action)::after {
    inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-xs) / -2);
  }
}
```

## Tokens it reads

```json
{
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "color": {
    "border": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      },
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    },
    "surface": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.100}}",
        "$description": "Standard surface: cards, panels, the header."
      },
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      }
    },
    "action": {
      "ghost": {
        "background-hover": {
          "$type": "color",
          "$value": "{{color.alpha.grey-800.15}}",
          "$description": "Ghost button: fill on hover."
        },
        "background-pressed": {
          "$type": "color",
          "$value": "{{color.action.outline.background-pressed}}",
          "$description": "Ghost button: fill while pressed, one step past hover; also the current page in pagination."
        }
      }
    },
    "code": {
      "keyword": {
        "$type": "color",
        "$value": "{{color.purple.600}}",
        "$description": "Code: control flow and modules, such as import, export, from, return, if and await, and CSS at-rules such as @media. Light purple/600, dark purple/400. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "declaration": {
        "$type": "color",
        "$value": "{{color.blue.600}}",
        "$description": "Code: words that declare or name, such as const, let, function, class, interface, type, new and this. Light blue/600, dark blue/400. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "string": {
        "$type": "color",
        "$value": "{{color.orange.600}}",
        "$description": "Code: text in quotes. Light orange/600, dark orange/400. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "number": {
        "$type": "color",
        "$value": "{{color.green.600}}",
        "$description": "Code: numbers, CSS lengths and colors, and true, false and null. Light green/600, dark green/300. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "function": {
        "$type": "color",
        "$value": "{{color.yellow.600}}",
        "$description": "Code: function names, CSS functions such as var(), and the command a shell line runs. Light yellow/600, dark yellow/300. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "type": {
        "$type": "color",
        "$value": "{{color.teal.600}}",
        "$description": "Code: type, class and component names. Light teal/600, dark teal/400. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "tag": {
        "$type": "color",
        "$value": "{{color.blue.600}}",
        "$description": "Code: HTML and JSX tags, and CSS selectors. Light blue/600, dark blue/400. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "property": {
        "$type": "color",
        "$value": "{{color.blue.800}}",
        "$description": "Code: keys and properties, such as object and JSON keys, CSS properties and custom properties, HTML and JSX attributes, and command flags. Light blue/800, dark blue/300. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "comment": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "Code: comments, the quietest part of the code. Light grey/600, dark grey/400. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      },
      "punctuation": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Code: brackets, commas, colons and operators. Light grey/700, dark grey/300. Text: 4.5:1 or more on the code block's well, in both themes; color is never the only sign of what a piece of code is."
      }
    }
  },
  "radius": {
    "container": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of cards, panels and dialogs, small."
      }
    },
    "control": {
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.4}}",
        "$description": "Corners of controls, small."
      }
    }
  },
  "space": {
    "inline": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things side by side, large."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      }
    },
    "inset": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Padding inside a component, extra small."
      },
      "xl": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Padding inside a component, extra large."
      }
    }
  },
  "size": {
    "icon": {
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      }
    },
    "control": {
      "xs": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Height of the extra-small control: 24 in both densities, the smallest target allowed. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  },
  "typography": {
    "code-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist Mono, Geist Mono Fallback, ui-monospace, monospace",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.6,
        "letterSpacing": "0px"
      },
      "$description": "Code MD: the code block and larger keys. Geist Mono with its own line height, 160%, so lines of code read apart."
    }
  },
  "motion": {
    "enter": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.320}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-out}}"
      }
    },
    "feedback": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.150}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-out}}"
      }
    }
  },
  "blur": {
    "4": {
      "$type": "dimension",
      "$value": "4px",
      "$description": "Raw blur, 4 px."
    }
  }
}
```

## Purpose

Code shown as code: monospace, preserved whitespace, each kind of code in its own color and each line numbered, in a well, with a bar above it for what the code is (a file name, or tabs for alternatives such as package managers) and small actions such as copy.

## Anatomy

- The block, a bordered container.
- The bar part, holding the tabs part or the label part at the start and the actions part at the end. With tabs the bar shows no label: the tabs name the code.
- The label part, the label component's subtle look: a file icon, the icon part, then the file name.
- The action part, a small icon button in a 24 px square inside the actions, filled like a ghost button while hovered or pressed, and faint when it cannot be used.
- The body part, the well.
- The numbers part, the line numbers in a column before the code, quieter than the code.
- The code part, the preformatted text in the code style, the mono face, always left to right.
- A part for each kind of code it colors: keyword, declaration, string, number, function, type, tag, property, comment and punctuation, each in its code color.
- The status part, a status line out of sight, in a block with actions.

## When to use

- Any code or command a reader may copy: installation lines, snippets, configuration.
- The Showroom's own snippets.

## When not to use

- Inline code in a sentence. Use the code element with the mono face.
- Output that arrives line by line as a program runs. Use a [log](log.md).

## Behavior

Given its language, the block colors each kind of code, in the manner of a familiar code editor: words that steer (`import`, `return`, `if`) in purple, words that declare (`const`, `function`, `class`) and tags and selectors in blue, text in quotes in orange, numbers in green, function names in yellow, types and components in teal, properties and keys in light blue, and comments and punctuation in gray. It reads CSS, HTML, JavaScript and TypeScript with or without JSX, JSON and shell commands, by their shape rather than their meaning, so an unusual line may stay one color; code in another language, or with no language given, stays one color. The language also picks the file icon before the label, or the label's file ending does. Each line is numbered unless the numbers are turned off; code of one line shows no number. The numbers are left out when the code is selected.

A page built without React colors its code the same way with the block's highlighter, beside the component, which cuts code into pieces and says each one's kind; markup written by hand puts each piece in a span with its part's class.

Tabs switch which snippet is shown; the page supplies the snippet for each. Actions call back; an action marked disabled takes no press. When the page gives an action another icon, such as a tick once its code is copied, the two icons cross-fade in the action's one place: the old one shrinks and blurs away and the new one comes in a little slower; under reduced motion they only cross-fade. The status line still says what happened. Long lines scroll sideways inside the well rather than wrapping. Code reads left to right on a right-to-left page too, its numbers on the left: markup written by hand gives the body and the code element `dir="ltr"`.

## Accessibility

### What it does for you

- The code is a real preformatted code element, read as code.
- Color is never the only way to tell code apart: the code reads the same without it, and each code color stands out from the well enough to read as text in both themes.
- The line numbers are hidden from screen readers and left out of a selection, so they are never read or copied with the code.
- Each action is a button with an accessible name.
- A block with actions keeps a status line out of sight, in the page from the start. A screen reader reads it whenever its words change, where many do not read a new name on the button that has focus (WCAG 2.2, 4.1.3 Status Messages).
- Tabs are the tabs component at its small size, a tab list with a name. The body is their one panel, named by the tab that shows it, and Tab reaches it, so a long line can be scrolled with the keyboard.
- Without tabs, a body whose code runs past it is a region named by the bar's label, or "Code" when there is none, and Tab reaches it, so it can be scrolled with the keyboard. A body that fits takes no Tab.
- A bare block shows no bar, but a label given to it still names its body.

### What you must do

- Put the result of an action in the status line (`status`, such as "Copied"), then empty it after a moment.
- Say what the tabs choose between (`tabsLabel`), which is "Language" unless told otherwise.
- Give blocks with bars on one page different labels, and give each bare block on a page a label, so a page of several blocks can tell them apart.

### How to test

- Tab through the block: each action takes focus, then the tab list, then a body that scrolls, with the focus ring showing in both themes and in a forced-color mode. Scroll a long line with the arrow keys.
- Press an action with Enter, then Space: each runs it once, and a screen reader says the status, such as "Copied".
- With a screen reader, the code is read as code, line numbers are not read, and tabs are read as a tab list named by `tabsLabel`.
- Select the code and copy it: line numbers are left out.
- With text at 200% and the page 320 pixels wide, the body scrolls sideways instead of cutting the code off.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus through the actions, then the tab list, then the body when the body scrolls or is the tabs' panel. |
| Enter or Space | Presses the focused action. |
| Left Arrow or Right Arrow | On a tab, moves to the previous or next tab and shows its code. On a body that scrolls, scrolls it sideways. |
| Up Arrow or Down Arrow | On a body that scrolls, scrolls it. |
| Home or End | On a tab, moves to the first or last tab. |

## Usage rules

### Do

- Give a copy action to any snippet meant to be pasted.
- Name the bar label after the file or the language.
- Say the language, so each kind of code takes its color.

### Don't

- Put anything but code in the well. Comments belong in prose above it.
