---
title: "render options"
description: "Per-call options on the renderer: classes, attributes, overlays, hooks and whitespace."
source: https://twinkleplop.pngwn.at/docs/render_options
---
# render options

Per-call options on the renderer: classes, attributes, overlays, hooks and whitespace.

Render options control the output of each call. Pass them as the second argument to a highlight function, or as the third argument to `to_html`.

```ts title="render.ts"
const ts = language();

const out = ts(code, {
  class_name: "my-code",
  line_numbers: { start: 10 },
  attributes: { "data-title": "math.ts", tabindex: 0 },
});
```

## block attributes

`class_name` replaces `twinkleplop` on the `<pre>`. `attributes` adds arbitrary attributes after the class, in the order given.

Do with this information what you will.

```ts title="attributes.ts"
ts(code, {
  attributes: {
    "data-title": "math.ts",  // data-title="math.ts"
    tabindex: 0,              // tabindex="0"
    hidden: true,             // hidden
    draggable: false,         // omitted
  },
});
```

- A string or number becomes `name="value"`,
- `true` becomes a bare attribute name,
- `false` emits nothing at all.

Values are HTML-escaped, so a value containing quotes cannot break out of the attribute.

> `class` and `style` are reserved. Use `class_name` for the class and CSS for styling. Passing a reserved or invalid attribute name throws a `TypeError`.

## overlays

Overlays add CSS classes to ranges of source code. You can add them through [directives](https://twinkleplop.pngwn.at/docs/directives) or the `overlays` option.

```ts title="overlays.ts"
ts(code, {
  overlays: [
    // by UTF-16 offset
    { start: 0, end: 12, class: "highlight" },
    // by line
    { line: 3, class: "diff-add" },
    // several lines, individually or as ranges
    { lines: [1, [5, 8]], class: "subdued" },
    // by line and character
    { start: { line: 2, character: 4 }, end: { line: 2, character: 9 }, class: "error" },
    // hide a range: it renders as spaces of equal width
    { start: 40, end: 60, hide: true },
    // write a range exactly as it is in the source, unescaped, as one token
    { start: 72, end: 80, verbatim: true },
  ],
});
```

Numeric positions are JavaScript string indices, the same as token positions. In the `{ line, character }` form, lines start at 1 and characters at 0. In both forms the range stops just before `end`, like `String.prototype.slice`.

Line-mode overlays add their class to every `span.l` the range matches. Token-mode overlays wrap the non-whitespace characters on each line. When overlays overlap, each region gets all the classes that apply to it. A hidden range renders as spaces of equal width, and a line that becomes whitespace-only is dropped. Visible line numbering continues without a gap.

A verbatim range is written exactly as it is in the source, with no escaping, as one `span.tok`. Template languages use it to keep an expression live inside escaped code. `type` sets its token class. Without it, the span takes the type of the token it sits inside, or none when it crosses tokens. It must stay on one line and must not overlap a hidden range or another verbatim range.

Every classification present also adds a `has-` class to the `<pre>`, so a theme can style the block as a whole. Turn that off with `has_classes: false`.

## line and token hooks

Use hooks to add classes or attributes to individual lines and tokens. Return nothing to leave an element unchanged.

```ts title="hooks.ts"
ts(code, {
  // n is the visible index, source_line the 1-based input line
  line: (n, source_line) => {
    if (source_line % 2 === 0) return { class: "even" };
  },
  // a decorated token renders as its own span and is never merged
  token: (type, start, end) => {
    if (type === "keyword") return { attrs: { "data-kw": true } };
  },
});
```

Returned classes go after the element's own classes, and returned attributes follow the same escaping rules as `attributes`. The `line` hook is not called in inline mode.

## inline structure

Use `structure: "inline"` for code inside a sentence. It omits the block and line elements and separates lines with `<br>`.

```ts title="inline.ts"
// no <pre>, no <code>, no line spans — <br> between lines
const snippet = ts("const x = 1", { structure: "inline" });

// drop it straight into prose
`Try <code>${snippet}</code> instead.`;
```

## whitespace and indent guides

By default whitespace between tokens is bare text. The options below wrap it so you can style it.

```ts title="whitespace.ts"
ts(code, {
  whitespace: "leading",      // "all" | "boundary" | "leading" | "trailing"
  indent_guides: { size: 4 }, // a tab is one level, 4 spaces is one level
});
```

`whitespace` wraps spaces and tabs between tokens, one span per character; whitespace inside a token stays part of that token.

`indent_guides` wraps each level of a line's leading indentation in its own `<span class="indent">`. Each tab is one level, and every `size` spaces (2 by default) is one level. Spaces left over after the last full level are not wrapped. The spans sit next to each other rather than inside each other, so styling `.indent` draws one guide per level.

```ts title="default.ts"
function total(items) {
  let sum = 0;
  for (const item of items) {
    if (item.active) {
      sum += item.price;
    }
  }
  return sum;
}
```

```ts title="indent_guides.ts"
function total(items) {
  let sum = 0;
  for (const item of items) {
    if (item.active) {
      sum += item.price;
    }
  }
  return sum;
}
```

```css title="whitespace.css"
.twinkleplop .space { white-space: pre; }
.twinkleplop .tab   { white-space: pre; }

/* indent guides are adjacent spans, one per level */
.twinkleplop .indent {
  box-shadow: inset 1px 0 0 var(--twp-comment);
}
```

## template escaping

The output escapes `& < > " '`. Template languages read more than that: Svelte and Angular treat `{` as an expression, Vue and Jinja treat `{{` as one. `escape` encodes extra characters everywhere text is escaped, including `class_name`, so the HTML can go straight into a template.

```ts title="escape.ts"
// safe to drop into a Svelte, Vue or Angular template
ts(code, { escape: { "{": "&#123;", "}": "&#125;" } });
```

Angular 17 control flow also reads `@`, so add `"@": "&#64;"` there.

Each key is one UTF-16 code unit and its value is written verbatim, so it must be valid HTML. A key can replace one of the five built-in entities too, so `"'": "&apos;"` works, and so does `"<": "<"`, which turns that escaping off. Line breaks, spaces and tabs can't be replaced.

[Verbatim ranges](https://twinkleplop.pngwn.at/docs/render_options#overlays) are left out, so an expression you mark live stays live while every other brace is encoded.

## HTML output

Block output uses this HTML structure:

```html title="output.html"
<pre class="twinkleplop"><code>
<span class="l"><span class="ln">1</span><span class="tok keyword">const</span> <span class="tok identifier">x</span></span>
<span class="l diff-add">...</span>
</code></pre>
```

- The block contains a `<pre>` and a `<code>` element, with a `span.l` for each visible line.
- Tokens use `span.tok` with a class for their type.
- Adjacent tokens of the same type share a span unless a hook adds classes or attributes.
- Text between tokens is escaped and rendered without a span.
