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!
function add(a: number, b: number): number { return a + b; } add(23, "42"); // ← This is wrong
function add(a: number, b: number): number { return a + b; // ← This is important } add(23, "42");
function add(a: number, b: number): number { return a + b; } add(23, "42"); // ← Watch this
function add(a: number, b: number): number { return a + b; } add(23, 42); // ← This works now!
function add(a: number, b: number): number { return a + b; } add(23, 42); // ← This returns ??
function add(a: number, b: number): number { return a + b; } add(23, 42); // ← This returns 65
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:
[ "Hello", "World" ]
[ "Hello", "World" ]
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.
A gutter decoration renders a single unicode character somewhere near the line number column. 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.
[ "Hello", "World" ]
[ "Hello", "World" ]
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:
data
object to set whatever attributes you want. You might want to use data.class
to add a custom class name that your CSS can then target.<span>
, which is a great attachment point for more custom styles (eg. the aforementioned custom font) or maybe even an idle animation.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.
[ "Hello", "World" ]
[ "Hello", "World" ]
[ "Hello", "World" ]
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;
/* Backdrop filter to desaturate and blur the affected tokens */
--cm-decoration-line-foreground-backdrop-filter: blur(0.75px) saturate(0);
/* Use an offset in order to not blur the line number */
--cm-decoration-line-foreground-offset-left: var(--line-numbers-column-width);
}
123456789/* Decoration for dead code */
.deadcode {
/* No line decoration background color */
--cm-decoration-line-background-background: none;
/* Backdrop filter to desaturate and blur the affected tokens */
--cm-decoration-line-foreground-backdrop-filter: blur(0.75px) saturate(0);
/* Use an offset in order to not blur the line number */
--cm-decoration-line-foreground-offset-left: var(--line-numbers-column-width);
}
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:
// Everthing after line 3 is dead code function example(x) { return null; return x; }
// This makes more sense! function example(x) { // return null; return x; }
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:
Text decorations decorate specific sections of code with backgrounds and/or underlines:
function add(a: number, b: number): number { return a + b; } add(23, 42); add(23n, 42n); // ← only accepts numbers
function add(a: number, b: number): number { return a + b; } add(23, 42); add(23n, 42n); // ← only accepts numbers
function add<T>(a: T, b: T): T { return a + b; // ← can't add ANY type } add(23, 42); add(23n, 42n); // ← this works now
function add<T>(a: T, b: T): T { // ← TOO generic return a + b; // ← can't add ANY type } add(23, 42); add(23n, 42n);
function add<T extends number | bigint>(a: T, b: T): T { return a + b; } add(23, 42); add(23n, 42n);
Every text decoration renders up to four elements per decorated section. Going from the background to the foreground this can include:
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:
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:
Decorations are rendered as regular HTML elements. To customize the element specifics you can set properties on the decorator's data
field.
tagName
to pick which HTML tag to use when rendering the element_
is used as an HTML attributeThis 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"
"_invisible": "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"
"_invisible": "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.
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;
}
Apart from the default styles for line and text decorations (a yellow-ish highlight), the built-in themes come with two additional presets for line and text decorations each: error
and ok
:
Default text decoration
"ok" text decoration
"error" text decoration
Default line decoration
"ok" line decoration
"error" line decoration
Default text decoration
"ok" text decoration
"error" text decoration
Default line decoration
"ok" line decoration
"error" line decoration
You can use the presets by setting data.class
on line or text decorations to contain the class names error
or ok
.
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:
[ "Hello", 42, "World" ]
[ "Hello", 42, "World" ]
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 21 to 28)
{ kind: "TEXT", data: {}, from: 21, to: 28 },
],
},
];
// ... 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 21 to 28)
{ kind: "TEXT", data: {}, from: 21, to: 28 },
],
},
];
// ... 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 21 to 28)
{ kind: "TEXT", data: { class: "salt" }, from: 21, to: 28 },
],
},
];
// ... do something with the keyframes
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 21 to 28)
{ kind: "TEXT", data: { class: "salt" }, from: 21, to: 28 },
],
},
];
// ... do something with the keyframes
It will therefore no longer transition a single decoration between frames, but rather remove the decoration from frame 1 and fade in a new yellow box:
[ "Hello", 42, "World" ]
[ "Hello", 42, "World" ]
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.