Decorations
Decorations highlight or obscure select lines or sections of code. Just like your IDE can highlight the current line, underline type errors or place icons the gutter, so can you!
Like with code, you add decorations declaratively to each code frame and let the library worry about computing the animation. The following example shows how a basic text decoration works:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 15 to 21)
{ kind: "TEXT", data: {}, from: 15, to: 22 },
],
},
];
import {
fromStringsToScene,
toAnimationHTML,
} from "@codemovie/code-movie/dist/index.js";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1">${toAnimationHTML(scene)}</code-movie-runtime>`;
123456789101112131415161718192021222324252627282930let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 15 to 21)
{ kind: "TEXT", data: {}, from: 15, to: 22 },
],
},
];
import {
fromStringsToScene,
toAnimationHTML,
} from "@codemovie/code-movie/dist/index.js";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1">${toAnimationHTML(scene)}</code-movie-runtime>`;
Result:
Decorations are a fairly powerful feature and not without some complexity. But since they can really elevate your animations to the next level, learning how they work is definitely worth it.
Decoration kinds
Gutter decorations
A gutter decoration renders a single unicode character somewhere near the line number colum. You can use the full range of unicode in general and emoji in particular to express things like errors ❌, approval ✅, personal opinion 🤮, additions ➕, deletions ➖, and much more.
The above example illustrates how the default gutter decoration style is pretty much just a static character. The example can be recreated like this:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Points to the line containing "Hello"
{ kind: "GUTTER", data: {}, text: "➡️", line: 2 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Points to the line containing "World"
{ kind: "GUTTER", data: {}, text: "➡️", line: 3 },
],
},
];
import { fromStringsToScene, toAnimationHTML } from "@codemovie/code-movie";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1">${toAnimationHTML(scene)}</code-movie-runtime>`;
123456789101112131415161718192021222324252627let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Points to the line containing "Hello"
{ kind: "GUTTER", data: {}, text: "➡️", line: 2 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Points to the line containing "World"
{ kind: "GUTTER", data: {}, text: "➡️", line: 3 },
],
},
];
import { fromStringsToScene, toAnimationHTML } from "@codemovie/code-movie";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1">${toAnimationHTML(scene)}</code-movie-runtime>`;
Some tips and tricks for gutter decorations:
- Use CSS to position gutter decorations on whichever side of the line number column you prefer.
- Change the font on gutter decorations to switch emoji styles or maybe even use a custom icon font. Remember that you can use the
data
object to set whatever attributes you want. You might want to usedata.class
to add a custom class name that your CSS can then target. - Gutter decorations render their contents in a nested
<span>
, which is a great attachment point for more custom styles (eg. the aforementioned custom font) or maybe even an idle animation.
Line decorations
A line decoration renders up to two boxes (one in the text foreground, one in the background) across one or more entire lines. You can use the background to highlight lines and the foreground to obscure content.
The above example illustrates how the default line decoration style is a subtle yellow highlight. The example can be recreated like this:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers the line containing "Hello"
{ kind: "LINE", data: {}, fromLine: 2, toLine: 2 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers the line containing "World"
{ kind: "LINE", data: {}, fromLine: 3, toLine: 3 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers both both lines
{ kind: "LINE", data: {}, fromLine: 2, toLine: 3 },
],
},
];
import { fromStringsToScene, toAnimationHTML } from "@codemovie/code-movie";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1 2">${toAnimationHTML(scene)}</code-movie-runtime>`;
12345678910111213141516171819202122232425262728293031323334let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers the line containing "Hello"
{ kind: "LINE", data: {}, fromLine: 2, toLine: 2 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers the line containing "World"
{ kind: "LINE", data: {}, fromLine: 3, toLine: 3 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers both both lines
{ kind: "LINE", data: {}, fromLine: 2, toLine: 3 },
],
},
];
import { fromStringsToScene, toAnimationHTML } from "@codemovie/code-movie";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1 2">${toAnimationHTML(scene)}</code-movie-runtime>`;
Two line decorations count as equal when their data
fields contain equal values.
The default line decoration highlights a line with a light yellow background. To add variants to this, write CSS rules targeting selectors that match any of the tag names and/or attributes set in the decoration's data
fields:
/* Decoration for dead code */
.deadcode {
/* No line decoration background color */
--cm-decoration-line-background-background: none;
/* Semi-transparent line decoration foreground color */
--cm-decoration-line-foreground-background: rgba(255, 255, 255, 0.333);
/* Backdrop filter for a slight blur effect */
--cm-decoration-line-foreground-backdrop-filter: blur(0.1em);
/* Set an offset in order to not blur the line number */
--cm-decoration-line-foreground-offset-left: calc(
var(--line-numbers-width) +
var(--cm-line-numbers-margin-left) +
var(--cm-content-margin-left)
);
}
123456789101112131415/* Decoration for dead code */
.deadcode {
/* No line decoration background color */
--cm-decoration-line-background-background: none;
/* Semi-transparent line decoration foreground color */
--cm-decoration-line-foreground-background: rgba(255, 255, 255, 0.333);
/* Backdrop filter for a slight blur effect */
--cm-decoration-line-foreground-backdrop-filter: blur(0.1em);
/* Set an offset in order to not blur the line number */
--cm-decoration-line-foreground-offset-left: calc(
var(--line-numbers-width) +
var(--cm-line-numbers-margin-left) +
var(--cm-content-margin-left)
);
}
To use this class just make sure that the decoration's data
field has an entry for class
or className
let keyframes = [
{
code: `\n// Everthing after line 3 is dead code\nfunction example(x) {\n return null;\n return x;\n}`,
decorations: [
// Covers the line containing "Hello"
{ kind: "LINE", data: { className: "deadcode" }, fromLine: 4, toLine: 4 },
],
},
];
123456789let keyframes = [
{
code: `\n// Everthing after line 3 is dead code\nfunction example(x) {\n return null;\n return x;\n}`,
decorations: [
// Covers the line containing "Hello"
{ kind: "LINE", data: { className: "deadcode" }, fromLine: 4, toLine: 4 },
],
},
];
Result:
The same approach works with text decorations. Check out the CSS variables that are available to tweak line decorations! The classes error
and ok
are available as built-in extra styles for both line and text decorations.
Some more tips and tricks for line decorations:
- Don't forget the foreground! Hiding or obscuring a few lines of code behind a line decoration foreground when teaching or presenting can be very useful.
- Combine decorations! An unobtrusive line background works great with a matching gutter decoration. If you want to indicate an error, add a red background and a gutter decoration like ❌ to direct your audience's attention.
Text decorations
Text decorations decorate specific sections of code with backgrounds and/or underlines:
Every text decoration renders up to four elements per decorated section. Going from the background to the foreground this can include:
- A background backdrop layer
- A background underline layer
- A foreground backdrop layer
- A foreground underline layer
The highlighted code is sandwiched between the background and foreground layers.
The above example illustrates how text decorations are made up of backgrounds and underlines. The example borders on non-trivial, but can be recreated like this:
Show keyframes for the above example
let keyframes = [
{
code: "function add(a: number, b: number): number {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← only accepts numbers",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 77,
to: 91,
},
],
},
{
code: "function add(a: number, b: number): number {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← only accepts numbers",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 16,
to: 22,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 27,
to: 33,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 36,
to: 42,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 77,
to: 91,
},
],
},
{
code: "function add<T>(a: T, b: T): T {\n return a + b; // ← can't add ANY type\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← this works now",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 19,
to: 20,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 25,
to: 26,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 29,
to: 30,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 35,
to: 48,
},
],
},
{
code: "function add<T>(a: T, b: T): T { // ← TOO generic\n return a + b; // ← can't add ANY type\n}\n\nadd(23, 42);\nadd(23n, 42n);",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 12,
to: 15,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 52,
to: 65,
},
],
},
{
code: "function add<T extends number | bigint>(a: T, b: T): T {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n);",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
class: "ok",
tagName: "mark",
},
from: 15,
to: 38,
},
{
kind: "GUTTER",
data: {
class: "gutter",
tagName: "mark",
},
line: 5,
text: "✅",
},
{
kind: "GUTTER",
data: {
class: "gutter",
tagName: "mark",
},
line: 6,
text: "✅",
},
],
},
];
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151let keyframes = [
{
code: "function add(a: number, b: number): number {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← only accepts numbers",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 77,
to: 91,
},
],
},
{
code: "function add(a: number, b: number): number {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← only accepts numbers",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 16,
to: 22,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 27,
to: 33,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 36,
to: 42,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 77,
to: 91,
},
],
},
{
code: "function add<T>(a: T, b: T): T {\n return a + b; // ← can't add ANY type\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← this works now",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 19,
to: 20,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 25,
to: 26,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 29,
to: 30,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 35,
to: 48,
},
],
},
{
code: "function add<T>(a: T, b: T): T { // ← TOO generic\n return a + b; // ← can't add ANY type\n}\n\nadd(23, 42);\nadd(23n, 42n);",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 12,
to: 15,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 52,
to: 65,
},
],
},
{
code: "function add<T extends number | bigint>(a: T, b: T): T {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n);",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
class: "ok",
tagName: "mark",
},
from: 15,
to: 38,
},
{
kind: "GUTTER",
data: {
class: "gutter",
tagName: "mark",
},
line: 5,
text: "✅",
},
{
kind: "GUTTER",
data: {
class: "gutter",
tagName: "mark",
},
line: 6,
text: "✅",
},
],
},
];
Note that the start and end indices from
and to
are the offsets of unicode code points, which may not correspond 1:1 with the character count. Two text decorations count as equal when their data
fields contain equal values.
The default text decoration highlights its range with a bright yellow background like a <mark>
element. Like with line decorations, you can write CSS rules targeting selectors that match any of the tag names and/or attributes set in the decoration's data
fields. The classes error
and ok
are available as built-in extra styles for both line and text decorations.
Some more tips and tricks for line decorations:
- Combine decorations! Text decorations works great with matching gutter decorations. If you want to indicate an error, add a red underline and a gutter decoration like ❌ to direct your audience's attention.
Decoration customization
Decorations are rendered as regular HTML elements. To customize the element specifics you can set properties on the decorator's data
field.
- specify
data.tagName
to pick a specific HTML tag to use for the element - every other property that does not start with
_
is used as an HTML attribute
This allows you to set up decorations as targets for your custom CSS and you can even attach scripted behavior by using a custom element as data.tagName
:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{
kind: "TEXT",
data: {
tagName: "mark", // use <mark> instead of the default <span>,
class: "customClass" // add "customClass" to the class attribute
"data-something": "42" // set attribute data-something="42"
"_nope": "42" // will not show up as an attribute
},
from: 4,
to: 11
},
],
},
];
12345678910111213141516171819let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{
kind: "TEXT",
data: {
tagName: "mark", // use <mark> instead of the default <span>,
class: "customClass" // add "customClass" to the class attribute
"data-something": "42" // set attribute data-something="42"
"_nope": "42" // will not show up as an attribute
},
from: 4,
to: 11
},
],
},
];
Every kind of decoration (gutter, line, text) has a different set of CSS variables available for styling purposes. By setting custom classes (or other attributes) on data.attributes
you can create your own custom set of differently-styled decorations. Personally, I like to use text decorations not just to highlight certain sections, but also to block out certain sections of code:
/* Keep the audience guessing */
.decoration-text-blackout {
/* Overlays the code being decorated */
--cm-decoration-text-foreground-background: black;
/* feTurbulence + feDisplacementMap from an inline SVG plus regular blur */
--cm-decoration-text-foreground-filter: url(#decoBackgroundFilter) blur(0.2px);
/* Undo the default yellow background (we use only the foreground) */
--cm-decoration-text-background-background: transparent;
}
/* Partially obscure unreachable code behind something */
.decoration-text-unreachable {
/* Overlays the code being decorated (assuming the background is also #fff) */
--cm-decoration-text-foreground-background: #fff;
/* Reduce opacity */
--cm-decoration-text-foreground-filter: opacity(50%);
/* Undo the default yellow background */
--cm-decoration-text-background-background: transparent;
}
12345678910111213141516171819/* Keep the audience guessing */
.decoration-text-blackout {
/* Overlays the code being decorated */
--cm-decoration-text-foreground-background: black;
/* feTurbulence + feDisplacementMap from an inline SVG plus regular blur */
--cm-decoration-text-foreground-filter: url(#decoBackgroundFilter) blur(0.2px);
/* Undo the default yellow background (we use only the foreground) */
--cm-decoration-text-background-background: transparent;
}
/* Partially obscure unreachable code behind something */
.decoration-text-unreachable {
/* Overlays the code being decorated (assuming the background is also #fff) */
--cm-decoration-text-foreground-background: #fff;
/* Reduce opacity */
--cm-decoration-text-foreground-filter: opacity(50%);
/* Undo the default yellow background */
--cm-decoration-text-background-background: transparent;
}
Note that different kinds of decorations do not share any CSS variable names. You can get by with just one class named .something
that hosts the settings for both text and line decorations:
.something {
/* Text */
--cm-decoration-text-foreground-underline-offset-y: -0.5;
--cm-decoration-text-foreground-underline-scale: 0.15;
--cm-decoration-text-foreground-underline-width: 10;
--cm-decoration-text-foreground-underline-squiggly: 1;
--cm-decoration-text-foreground-underline-color: #c00;
--cm-decoration-text-background-background: none;
/* Line */
--cm-decoration-line-background-background: #ffeeee;
}
1234567891011.something {
/* Text */
--cm-decoration-text-foreground-underline-offset-y: -0.5;
--cm-decoration-text-foreground-underline-scale: 0.15;
--cm-decoration-text-foreground-underline-width: 10;
--cm-decoration-text-foreground-underline-squiggly: 1;
--cm-decoration-text-foreground-underline-color: #c00;
--cm-decoration-text-background-background: none;
/* Line */
--cm-decoration-line-background-background: #ffeeee;
}
Decoration defaults
Apart from the default styles for line and text decorations, the default theme comes with two additional presets for line and text decorations each: error
and ok
:
You can use the presets by setting data.class
on line or text decorations to contain the class names error
or ok
.
Decorations and the diffing algorithm
From the perspective of the diffing algorithm, every decoration is represented as a hash constructed from its kind
and data
fields. Two decorations of the same kind
and with equivalent data
fields are therefore treated as interchangeable. Consider how the text decoration in the following example moves between frames:
This is happens because the decoration object has the same kind
and equivalent data
in both frames:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 15 to 21)
{ kind: "TEXT", data: {}, from: 15, to: 22 },
],
},
];
// ... do something with the keyframes
123456789101112131415161718let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 15 to 21)
{ kind: "TEXT", data: {}, from: 15, to: 22 },
],
},
];
// ... do something with the keyframes
If we add something to the data
object in the second frame, we "salt" the decoration's hash and cause the diffing algorithm to consider the decoration object to be distinct:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 15 to 21)
{ kind: "TEXT", data: { class: "salt" }, from: 15, to: 22 },
],
},
];
// ... do something with the keyframes
123456789101112131415161718let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 15 to 21)
{ kind: "TEXT", data: { class: "salt" }, from: 15, to: 22 },
],
},
];
// ... do something with the keyframes
It will therefore no longer transition a single decoration between frames, but rather remove the decoration frame 1 and fade in a new yellow box:
This enables opt-in manual control over how decorations behave. In extremis, you could just add an ID to every decoration and exert maximum manual control, but the diffing algorithm should usually do something reasonable by default.