All patterns

Code Block Highlight Load

Plain code gains its syntax colors in one soft pass once the highlighter finishes.

loadingfuturisticsubtleautomatic · finite · intermediate · ~0.7s
Variant

The animated component in this preview is rendered from the canonical file shown here. The surrounding demo shell only provides context and is not part of the copied code.

309 lines · react + motion only
import { useEffect, useState } from "react";
import { motion, useReducedMotion } from "motion/react";

/**
 * Vibary · Code Block Highlight Load
 *
 * Code renders immediately as plain text — readable from the first
 * frame — and gains its syntax colors in one pass down the block once
 * the highlighter has finished.
 *
 * Self-contained: depends only on `motion` (react ships with your app).
 * The block surface and the punctuation are mixed from the inherited
 * text color, so it reads on a light page and on a dark one; the syntax
 * hues are literal mid-tones chosen to hold up in either theme.
 * Works with zero props; pass `highlighted` to drive it from your own
 * tokenizer.
 * Requires the automatic JSX runtime (default since React 17).
 */

export type CodeBlockHighlightLoadProps = {
  /** Visual character of the motion. */
  variant?: "subtle" | "default" | "playful";
  /** True once tokens are ready. Left undefined, the component colors
   *  itself after `highlightAfterMs`. */
  highlighted?: boolean;
  /** Only consulted while `highlighted` is undefined. */
  highlightAfterMs?: number;
  /** Name on the block's header. */
  fileName?: string;
  /** Language chip revealed once the pass completes. */
  language?: string;
  /** Block width — px number or any CSS length. */
  width?: number | string;
};

type VariantConfig = {
  /** Seconds between one line taking color and the next. */
  lineStep: number;
  fadeSeconds: number;
  /** Opacity of the uncolored text before the pass. */
  plainOpacity: number;
};

// Quality rule: nothing moves. Not one pixel of travel, no scale, no
// weight change — the block is dense text, and text that shifts while
// you are reading it is worse than text with no color at all. The whole
// pattern is a staggered cross-fade between two identically laid out
// copies of the same lines.
const VARIANTS: Record<"subtle" | "default" | "playful", VariantConfig> = {
  // Near-simultaneous, so the color simply appears. For documentation
  // pages with several blocks on screen at once.
  subtle: {
    lineStep: 0.022,
    fadeSeconds: 0.22,
    plainOpacity: 0.72,
  },
  // A readable pass from the first line to the last. All-purpose.
  default: {
    lineStep: 0.045,
    fadeSeconds: 0.3,
    plainOpacity: 0.6,
  },
  // A slower sweep for a single hero snippet, where watching the block
  // resolve is part of the point.
  playful: {
    lineStep: 0.075,
    fadeSeconds: 0.36,
    plainOpacity: 0.5,
  },
};

type Kind =
  | "plain"
  | "keyword"
  | "fn"
  | "string"
  | "number"
  | "type"
  | "punct"
  | "comment";

/** Theme-adaptive neutral: `currentColor` is the inherited text color, so
 *  the surface, the gutter and the punctuation stay correctly toned on
 *  light and dark pages. */
const tone = (percent: number) =>
  `color-mix(in srgb, currentColor ${percent}%, transparent)`;

/** Syntax hues are deliberately mid-tone rather than borrowed from a
 *  dark-only theme: the same six colors have to clear contrast on a
 *  near-white page and a near-black one, because this block inherits
 *  whatever surface it is pasted onto. */
const SYNTAX: Record<Kind, string> = {
  plain: tone(88),
  keyword: "#A472E8",
  fn: "#4C93EF",
  string: "#2FA37B",
  number: "#D08B2C",
  type: "#3EA3C4",
  punct: tone(52),
  comment: tone(42),
};

/** Embedded sample, tokenized by hand so the file needs no highlighter.
 *  Replace it with your own tokens; the motion does not care where they
 *  came from. */
const LINES: { text: string; kind: Kind }[][] = [
  [{ text: "// Invoices for one workspace, newest first", kind: "comment" }],
  [
    { text: "export", kind: "keyword" },
    { text: " ", kind: "plain" },
    { text: "async", kind: "keyword" },
    { text: " ", kind: "plain" },
    { text: "function", kind: "keyword" },
    { text: " ", kind: "plain" },
    { text: "listInvoices", kind: "fn" },
    { text: "(", kind: "punct" },
    { text: "id", kind: "plain" },
    { text: ": ", kind: "punct" },
    { text: "string", kind: "type" },
    { text: ") {", kind: "punct" },
  ],
  [
    { text: "  ", kind: "plain" },
    { text: "const", kind: "keyword" },
    { text: " res = ", kind: "punct" },
    { text: "await", kind: "keyword" },
    { text: " ", kind: "plain" },
    { text: "fetch", kind: "fn" },
    { text: "(", kind: "punct" },
    { text: '"/api/invoices?workspace="', kind: "string" },
    { text: " + id);", kind: "punct" },
  ],
  [
    { text: "  ", kind: "plain" },
    { text: "if", kind: "keyword" },
    { text: " (!res.ok) ", kind: "punct" },
    { text: "throw", kind: "keyword" },
    { text: " ", kind: "plain" },
    { text: "new", kind: "keyword" },
    { text: " ", kind: "plain" },
    { text: "Error", kind: "fn" },
    { text: "(", kind: "punct" },
    { text: '"Could not load invoices"', kind: "string" },
    { text: ");", kind: "punct" },
  ],
  [
    { text: "  ", kind: "plain" },
    { text: "const", kind: "keyword" },
    { text: " page = ", kind: "punct" },
    { text: "await", kind: "keyword" },
    { text: " ", kind: "plain" },
    { text: "res.json", kind: "fn" },
    { text: "();", kind: "punct" },
  ],
  [
    { text: "  ", kind: "plain" },
    { text: "return", kind: "keyword" },
    { text: " page.items.", kind: "punct" },
    { text: "slice", kind: "fn" },
    { text: "(", kind: "punct" },
    { text: "0", kind: "number" },
    { text: ", ", kind: "punct" },
    { text: "20", kind: "number" },
    { text: ") ", kind: "punct" },
    { text: "as", kind: "keyword" },
    { text: " ", kind: "plain" },
    { text: "Invoice[]", kind: "type" },
    { text: ";", kind: "punct" },
  ],
  [{ text: "}", kind: "punct" }],
];

const MONO =
  'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace';

export default function CodeBlockHighlightLoad({
  variant = "default",
  highlighted,
  highlightAfterMs = 800,
  fileName = "invoices.ts",
  language = "TypeScript",
  width = 356,
}: CodeBlockHighlightLoadProps) {
  const reduceMotion = useReducedMotion();
  const cfg = VARIANTS[variant];
  const [selfOn, setSelfOn] = useState(false);

  // Uncontrolled by default so the file runs on its own; the moment a
  // caller passes `highlighted`, this timer stays out of the way.
  useEffect(() => {
    if (highlighted !== undefined) return;
    const timer = setTimeout(() => setSelfOn(true), highlightAfterMs);
    return () => clearTimeout(timer);
  }, [highlighted, highlightAfterMs]);

  const on = highlighted ?? selfOn;
  // Reduced motion: the colors are simply there. Nothing was moving in
  // the first place, so all that is dropped is the pass down the block.
  const step = reduceMotion ? 0 : cfg.lineStep;
  const fade = reduceMotion ? 0.001 : cfg.fadeSeconds;
  const tail = step * LINES.length;

  return (
    <div
      style={{
        width,
        borderRadius: 12,
        overflow: "hidden",
        background: tone(5),
        border: `1px solid ${tone(11)}`,
        boxSizing: "border-box",
      }}
    >
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
          gap: 12,
          padding: "8px 12px",
          borderBottom: `1px solid ${tone(9)}`,
          fontSize: 11.5,
        }}
      >
        <span style={{ fontFamily: MONO, opacity: 0.6 }}>{fileName}</span>
        <motion.span
          initial={false}
          animate={{ opacity: on ? 1 : 0 }}
          transition={{ duration: 0.24, ease: "easeOut", delay: on ? tail : 0 }}
          style={{
            padding: "2px 7px",
            borderRadius: 6,
            background: tone(8),
            fontWeight: 600,
            letterSpacing: 0.2,
          }}
        >
          {language}
        </motion.span>
      </div>

      <pre
        style={{
          margin: 0,
          padding: "11px 12px 13px",
          overflowX: "auto",
          fontFamily: MONO,
          fontSize: 12.5,
          lineHeight: 1.65,
        }}
      >
        <code>
          {LINES.map((line, lineIndex) => {
            const delay = on ? lineIndex * step : 0;
            return (
              <span
                key={lineIndex}
                style={{ display: "flex", alignItems: "baseline" }}
              >
                <span
                  aria-hidden
                  style={{
                    flexShrink: 0,
                    width: 20,
                    textAlign: "right",
                    marginRight: 12,
                    color: tone(26),
                    userSelect: "none",
                  }}
                >
                  {lineIndex + 1}
                </span>

                {/* The two copies are metrically identical — same family,
                    same size, same weight, no italics — and the family is
                    monospaced, so every glyph in the colored copy lands on
                    the exact pixel its plain twin occupied. That is the
                    whole trick: with the layout guaranteed, the pass can be
                    a plain cross-fade and never a re-flow. */}
                <span style={{ display: "grid", whiteSpace: "pre" }}>
                  <motion.span
                    initial={false}
                    animate={{ opacity: on ? 0 : cfg.plainOpacity }}
                    transition={{ duration: fade, ease: "easeOut", delay }}
                    style={{ gridArea: "1 / 1" }}
                  >
                    {line.map((token) => token.text).join("")}
                  </motion.span>
                  <motion.span
                    initial={false}
                    animate={{ opacity: on ? 1 : 0 }}
                    transition={{ duration: fade, ease: "easeOut", delay }}
                    style={{ gridArea: "1 / 1" }}
                  >
                    {line.map((token, tokenIndex) => (
                      <span key={tokenIndex} style={{ color: SYNTAX[token.kind] }}>
                        {token.text}
                      </span>
                    ))}
                  </motion.span>
                </span>
              </span>
            );
          })}
        </code>
      </pre>
    </div>
  );
}

About this pattern

Syntax highlighting almost always arrives late — a worker, a lazily imported grammar, a streamed response — and the usual fix is to hide the snippet until it is ready. This does the opposite: the code is readable as plain text from the first frame and takes color line by line once the tokens land. Nothing moves. Two copies of each line, one plain and one colored, sit in the same grid cell and cross-fade; because the family is monospaced and neither copy changes weight or slant, every glyph in the colored version lands on the exact pixel its plain twin occupied, so the pass can never re-flow a block someone is already reading. The hues are literal mid-tones rather than a dark-theme palette, since the block inherits whatever surface it is pasted onto.

API reference snippetDocs page code sampleStreamed code answerChangelog diff view

Where it shows up

Screens we drew to show where this motion usually sits. Illustrations, not captures of any product.

  • Ridgeline
    Changes
    Commits
    Checks
    Files
    Conversation
    ChangesNew
    Brand refresh.figPriya Raman · 2.4 MB
    Q3 planning.pdfMarcus Bell · 840 KB
    Supplier contract.docxDana Whitfield · 96 KB
    Photography brief.mdNils Bergström · 12 KB
    Invoice 4821.pdfBilling · 64 KB
    Code review

    A file renders as plain text first and picks up its colors a moment later.

Related patterns