Theme
On this page (15)
Component Skill — components/mockup-code/component-skill.md

Native basis

A .mockup-code of <pre><code> lines; data-prefix on a line draws its prompt with ::before - generated content is never selected or copied.

Web Platform APIs

attr()counter()user-selectcolor-mix()unicode-bidi

Classes

.mockup-code

Data attributes

Window: data-chrome (mac, none), data-title, data-numbers, data-wrap, data-strike, data-animate, data-variant (muted, outline, primary), data-size (sm, lg); lines: data-prefix, data-tone (success, warning, info, destructive, muted), data-highlight (solid), data-cursor, data-diff (add, remove); <ins> / <del> for word changes.

§Prompt

data-prefix='$' draws the prompt. It is generated content: select the line or copy it - you get the command, never the $.

§Output and status

Lines after the command are its output: data-tone colours them - warning, success, info, destructive, muted - tuned for the dark surface.

§Highlighted lines

data-highlight tints a line in its tone; data-highlight='solid' fills it - the error line. Numbers as prefixes label the lines.

§Long lines

A long line scrolls the window sideways (the lines keep one width, so the scroll is shared); data-wrap wraps instead, with a hanging indent under the prompt.

§Without prefix

A line without data-prefix has no prompt - plain output, or a file.

§Colors

data-variant: muted and outline for a light terminal (the tones darken for contrast), primary for any brand colour.

§Window and title

data-chrome='mac' colours the dots, data-title puts a title in the bar, data-chrome='none' drops the bar.

§Line numbers and syntax highlighting

data-numbers numbers every line without its own prefix - CSS counters, nothing to keep in sync. The colours come from Shiki (the highlighter these docs use): codeToTokens returns one token list per line, which maps one-to-one onto the window's <pre> lines - plain text until it loads, and the numbers and the highlighted line keep working.

§Diff

data-diff='remove' and 'add' mark the lines - a generated -/+ in its own sign column, red and green bands; lines without it are context and line up. <del> and <ins> inside a line mark the words that changed. Numbers and the sign column combine.

§Animated diff

data-animate plays the change: removed lines turn red and strike through, then the added lines slide in green, one after another (sibling-index() staggers them; --mockup-code-delay / --mockup-code-stagger tune it). CSS only - the script here only starts it when the window scrolls into view and replays it on click.

§Multi-line commands

One command per line; a command continued with a backslash goes on lines with data-prefix='' - the prompt column stays empty, so everything lines up, and a copy is still a valid shell command.

§Cursor

data-cursor adds a blinking block after the line - a prompt waiting for input (still under reduced motion).

§Sizes

data-size sm · (default) · lg - text, padding and the bar scale together.

§In a right-to-left page

Commands read left to right everywhere: the window isolates its direction, so an Arabic page still shows the shell as it is typed.

§CSS view file

/* -- Mockup Code component --------------------------------------------- */
/* A terminal / code window: a stack of <pre> lines under window chrome.
   Prompts, line numbers and the cursor are CSS-generated, so selecting and
   copying the text never picks them up. CSS only. */
@layer components {
  .mockup-code {
    /* surface + the line tones, retuned per variant for its contrast */
    --_bg: oklch(0.21 0.006 286);
    --_fg: oklch(0.94 0 0);
    --_dim: oklch(0.68 0.01 286);
    --_success: oklch(0.8 0.16 150);
    --_warning: oklch(0.84 0.15 80);
    --_info: oklch(0.76 0.13 245);
    --_destructive: oklch(0.72 0.18 25);
    --_px: 1.25rem;
    position: relative;
    box-sizing: border-box;
    min-width: 0;
    max-width: 100%;
    padding-block: 1.25rem 1rem;
    border-radius: var(--radius-xl);
    background: var(--_bg);
    color: var(--_fg);
    font-family: var(--font-mono);
    font-size: 0.875rem;
    line-height: 1.7;
    /* code shows what is typed: no => → ⇒ or <!-- → arrow ligatures */
    font-variant-ligatures: none;
    text-align: start;
    /* code reads left to right even inside an RTL page */
    direction: ltr;
    unicode-bidi: isolate;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    /* one column as wide as the longest line (at least the window): every
       line - and its highlight band - spans the same width while the
       window scrolls sideways */
    display: grid;
    grid-template-columns: minmax(100%, max-content);
    /* -- Window chrome: three dots; data-chrome="mac" colours them,
       data-chrome="none" drops the bar ---------------------------- */
    &::before {
      content: "";
      display: block;
      width: 0.75rem;
      height: 0.75rem;
      margin-inline-start: var(--_px);
      margin-block-end: 1rem;
      border-radius: 9999px;
      color: var(--_dim);
      background: currentColor;
      opacity: 0.45;
      box-shadow: 1.25rem 0 0 currentColor, 2.5rem 0 0 currentColor;
    }
    &[data-chrome="mac"]::before {
      opacity: 1;
      background: oklch(0.68 0.2 25);
      box-shadow: 1.25rem 0 0 oklch(0.82 0.16 85), 2.5rem 0 0 oklch(0.74 0.17 145);
    }
    &[data-chrome="none"] { padding-block-start: 1rem; }
    &[data-chrome="none"]::before { display: none; }
    /* -- A title in the bar: data-title="bash - 80x24" -------------- */
    &[data-title]:not([data-chrome="none"])::after {
      content: attr(data-title);
      position: absolute;
      inset-block-start: 0.75rem;
      inset-inline: 4.5rem;
      font-family: var(--font-sans);
      font-size: 0.75rem;
      line-height: 1.25rem;
      text-align: center;
      color: var(--_dim);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      pointer-events: none;
    }
    /* -- Lines -------------------------------------------------- */
    & > pre {
      box-sizing: border-box;
      margin: 0;
      padding: 0 var(--_px);
      border: 0;
      border-radius: 0;
      background: transparent;
      color: inherit;
      font: inherit;
      white-space: pre;
    }
    & > pre > code {
      padding: 0;
      border: 0;
      background: none;
      color: inherit;
      font: inherit;
    }
    /* the prompt: $, >, ~, a number - generated, never selected or copied */
    & > pre[data-prefix]::before,
    &[data-numbers] > pre::before {
      display: inline-block;
      width: 2ch;
      margin-inline-end: 1.25ch;
      text-align: end;
      color: var(--_dim);
      user-select: none;
    }
    & > pre[data-prefix]::before { content: attr(data-prefix); }
    /* data-numbers on the window: every line without its own prefix is
       numbered (CSS counters - nothing to keep in sync) */
    &[data-numbers] { counter-reset: mockup-line; }
    &[data-numbers] > pre { counter-increment: mockup-line; }
    &[data-numbers] > pre:not([data-prefix])::before { content: counter(mockup-line); }
    /* -- Tones: output lines --------------------------------------- */
    & > pre[data-tone] { color: var(--_tone); }
    & > pre[data-tone="success"] { --_tone: var(--_success); }
    & > pre[data-tone="warning"] { --_tone: var(--_warning); }
    & > pre[data-tone="info"] { --_tone: var(--_info); }
    & > pre[data-tone="destructive"] { --_tone: var(--_destructive); }
    & > pre[data-tone="muted"] { --_tone: var(--_dim); }
    /* -- Highlighted line: a tinted band in the line's tone (or the
       text colour); data-highlight="solid" fills it -------------- */
    & > pre[data-highlight] {
      background: color-mix(in oklch, var(--_tone, var(--_fg)) 16%, transparent);
    }
    & > pre[data-highlight="solid"] {
      background: var(--_tone, var(--_fg));
      color: var(--_bg);
      &::before { color: inherit; opacity: 0.7; }
    }
    /* -- Diff: data-diff="add" / "remove" lines ----------------------
       The sign sits in its own column before the code (generated, never
       copied), so numbers and prompts keep theirs; every line of a diff
       window gets the column, so context lines line up. Word changes
       inside a line are native <ins> / <del>. */
    &:where(:has(> pre[data-diff])) > pre > code::before {
      content: "";
      display: inline-block;
      width: 2ch;
      user-select: none;
    }
    & > pre[data-diff="add"] { --_tone: var(--_success); }
    & > pre[data-diff="remove"] { --_tone: var(--_destructive); }
    & > pre[data-diff] {
      color: var(--_tone);
      background: color-mix(in oklch, var(--_tone) 14%, transparent);
    }
    & > pre[data-diff="add"] > code::before { content: "+"; }
    & > pre[data-diff="remove"] > code::before { content: "-"; }
    & > pre > code :is(ins, del) {
      border-radius: 0.2em;
      color: inherit;
      text-decoration: none;
    }
    & > pre > code ins { background: color-mix(in oklch, var(--_success) 32%, transparent); }
    & > pre > code del {
      background: color-mix(in oklch, var(--_destructive) 32%, transparent);
      text-decoration: line-through;
    }
    /* data-strike (and the animated diff) strikes removed lines through */
    &:is([data-strike], [data-animate]) > pre[data-diff="remove"] {
      opacity: 0.8;
      & > code {
        text-decoration: line-through;
        text-decoration-color: color-mix(in oklch, var(--_tone) 70%, transparent);
      }
    }
    /* -- Animated diff: removed lines turn red and strike through, added
       lines slide in green, one after another; context lines stay.
       Replays whenever data-animate is set again. ------------------ */
    &[data-animate] > pre {
      --_stagger: var(--mockup-code-stagger, 140ms);
      --_delay: calc(var(--mockup-code-delay, 300ms) + var(--_i, 0) * var(--_stagger));
    }
    &[data-animate] > pre[data-diff="remove"] {
      animation: mockup-code-remove 450ms ease-out var(--_delay) both;
      & > code { animation: mockup-code-strike 450ms ease-out var(--_delay) both; }
    }
    &[data-animate] > pre[data-diff="add"] {
      animation: mockup-code-add 450ms cubic-bezier(0.2, 0.8, 0.2, 1) var(--_delay) both;
    }
    /* -- Cursor: a blinking block after the line --------------------- */
    & > pre[data-cursor] > code::after {
      content: "";
      display: inline-block;
      width: 0.6em;
      height: 1.15em;
      margin-inline-start: 0.1em;
      vertical-align: text-bottom;
      background: currentColor;
      animation: mockup-code-cursor 1s steps(1) infinite;
    }
    /* -- Wrap instead of scrolling --------------------------------- */
    &[data-wrap] { grid-template-columns: minmax(0, 1fr); }
    &[data-wrap] > pre {
      white-space: pre-wrap;
      overflow-wrap: anywhere;
      padding-inline-start: calc(var(--_px) + 3.25ch);
      text-indent: -3.25ch;
    }
    &[data-wrap] > pre:not([data-prefix]):not(:where([data-numbers] > pre)) {
      padding-inline-start: var(--_px);
      text-indent: 0;
    }
    /* -- Variants (surfaces) --------------------------------------- */
    &[data-variant="muted"],
    &[data-variant="outline"] {
      --_fg: var(--foreground);
      --_dim: var(--muted-foreground);
      --_success: oklch(0.52 0.14 150);
      --_warning: oklch(0.58 0.14 70);
      --_info: oklch(0.52 0.14 250);
      --_destructive: var(--destructive);
    }
    &[data-variant="muted"] { --_bg: var(--muted); }
    &[data-variant="outline"] {
      --_bg: var(--card);
      border: 1px solid var(--border);
    }
    &[data-variant="primary"] {
      --_bg: var(--primary);
      --_fg: var(--primary-foreground);
      --_dim: color-mix(in oklch, var(--primary-foreground) 65%, var(--primary));
    }
    /* -- Sizes ------------------------------------------------------ */
    &[data-size="sm"] { font-size: 0.75rem; --_px: 1rem; padding-block: 1rem 0.75rem; }
    &[data-size="lg"] { font-size: 1rem; --_px: 1.5rem; }
  }
  @keyframes mockup-code-cursor {
    50% { opacity: 0; }
  }
  /* each keyframe starts neutral and ends on the line's own diff styles */
  @keyframes mockup-code-remove {
    from { color: var(--_fg); background: transparent; opacity: 1; }
  }
  @keyframes mockup-code-strike {
    from { text-decoration-color: transparent; }
  }
  @keyframes mockup-code-add {
    from { color: var(--_fg); background: transparent; opacity: 0; translate: -1.5rem 0; }
  }
  /* the line's position drives the stagger: sibling-index() where the
     browser has it, a counted fallback for the first 24 lines elsewhere */
  @supports (order: sibling-index()) {
    .mockup-code[data-animate] > pre { --_i: sibling-index(); }
  }
  @supports not (order: sibling-index()) {
    .mockup-code[data-animate] > pre:nth-child(2) { --_i: 2; }
    .mockup-code[data-animate] > pre:nth-child(3) { --_i: 3; }
    .mockup-code[data-animate] > pre:nth-child(4) { --_i: 4; }
    .mockup-code[data-animate] > pre:nth-child(5) { --_i: 5; }
    .mockup-code[data-animate] > pre:nth-child(6) { --_i: 6; }
    .mockup-code[data-animate] > pre:nth-child(7) { --_i: 7; }
    .mockup-code[data-animate] > pre:nth-child(8) { --_i: 8; }
    .mockup-code[data-animate] > pre:nth-child(9) { --_i: 9; }
    .mockup-code[data-animate] > pre:nth-child(10) { --_i: 10; }
    .mockup-code[data-animate] > pre:nth-child(11) { --_i: 11; }
    .mockup-code[data-animate] > pre:nth-child(12) { --_i: 12; }
    .mockup-code[data-animate] > pre:nth-child(13) { --_i: 13; }
    .mockup-code[data-animate] > pre:nth-child(14) { --_i: 14; }
    .mockup-code[data-animate] > pre:nth-child(15) { --_i: 15; }
    .mockup-code[data-animate] > pre:nth-child(16) { --_i: 16; }
    .mockup-code[data-animate] > pre:nth-child(n + 17) { --_i: 17; }
  }
}
@media (prefers-reduced-motion: reduce) {
  @layer components {
    .mockup-code > pre[data-cursor] > code::after { animation: none; }
    /* the diff shows its end state at once */
    .mockup-code[data-animate] > pre,
    .mockup-code[data-animate] > pre > code { animation: none; }
  }
}
@media (prefers-contrast: more) {
  @layer components {
    .mockup-code { --_dim: var(--_fg); border: 1px solid var(--_fg); }
  }
}
@media (forced-colors: active) {
  @layer components {
    .mockup-code { border: 1px solid CanvasText; }
    .mockup-code > pre[data-highlight] { forced-color-adjust: none; background: Highlight; color: HighlightText; }
    /* the +/- signs carry the diff; <ins>/<del> get system marks */
    .mockup-code > pre > code :is(ins, del) { outline: 1px solid CanvasText; }
  }
}

Comments, ideas or improvements? Edit this page's source on GitHub