Code Block Highlight Load
Plain code gains its syntax colors in one soft pass once the highlighter finishes.
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.
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.
Where it shows up
Screens we drew to show where this motion usually sits. Illustrations, not captures of any product.
- Code review
A file renders as plain text first and picks up its colors a moment later.
Related patterns
- Avatar Group LoadOverlapping faces deal themselves out along the stack, then the overflow count lands.
- Background Refresh HintA two-pixel tinted band travels the panel's top edge while data refetches, without interrupting reading.
- Cache Hit InstantCached content gets no entrance at all; only the values that actually changed animate.