Skip to main content
Recipe number 180

Cookbook · Chapter 2 · Type & text

Colour-coded highlights in a study reader

A history reader whose key words carry three highlight colours (green for terms, amber for dates, blue for names) from three markStyles in the palette.

  • Trim 170 × 240 mm
  • 1 column
  • Source Serif 4 10.5/14.5
  • Source Sans 3
  • 4 pages
  • Level
  • Postext 1.65.0
  • Laid out in 127 ms
  • 164 lines of code

In short

A school reading on the history of printing. Key terms are marked in green, dates in amber and people and places in blue, and a key on the first page tells the reader what each colour means.

What you'll build

Reading 2 of a unit in a school history reader, Readings in Early Modern History: four pages on how printing spread from Mainz across Europe between 1450 and 1550, on a 170 × 240 mm page. The editors have marked the text the way a careful student would, with a code that stays the same through the whole book: key terms on a green band, dates on amber, people and places on pale blue. A key under the title explains the code with the three colours themselves, and the glossary on the last page opens each entry with its green term. The marks are bands under ordinary text, so the lines break, hyphenate and justify as if the bands were not there.

This recipe answers

  • How do I highlight the key words of a study text in colours that each mean something (terms, dates, names)?

The short answer

script.js · lines 37–52in full code
// :mark[Mainz]{style="name"} in the text picks a style by id. The band is painted under
// words that stay ordinary text, so the lines break, hyphenate and justify as if the
// mark were not there, and a mark that runs over a line end gets a band on each line.
const band = (id, name) => ({
  id, name,
  background: col(id), // pale enough for ink at full contrast on top
  color: col('ink'), // the words keep the colour of the text
  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
  paddingX: em(0.12), // the band reaches a little past the first and the last letter
  paddingTop: em(0.04), paddingBottom: em(0.1), // and down past the descenders
});
const markStyles = [
  band('term', t({ en: 'Key term', es: 'Término clave' })),
  band('date', t({ en: 'Date', es: 'Fecha' })),
  band('name', t({ en: 'Name', es: 'Nombre' })),
];

Three highlight styles, one per kind of word, linked to the palette

Ingredients

Type
Source Serif 4, Source Sans 3 (SIL OFL 1.1)
Assets
  • press-1400.jpg
  • The printing workshop on the opening page, a painting (Generated With Diffusion Models, original)

Method

#1 · One style for each kind of word

The code is the short answer above. markStyles declares three named styles, and the text picks one per mark: :mark[Johann Fust]{style="name"}, :mark[1455]{style="date"}, :mark[colophon]{style="term"} (highlight marks). Each style names what it means (name is what an editor sees), so the Markdown records why a word is marked, not which colour it wears. Change the colour of a style and every date in the book changes with it. The ends are square, as since postext 1.52 by default (highlight styles): a highlighter pen leaves square ends, and a rounded band reads as a chip.

#2 · Three tints from the palette

script.js · lines 14–29in full code
const palette = {
  ink: '#1f1c19', // text: a warm near-black
  accent: '#8a3220', // kickers, rules, numbers: an iron-gall red
  tint: '#f5f0e7', // the key's background
  rule: '#d3cabc', // hairlines
  muted: '#6b645b', // running heads, colophon
  paper: '#ffffff',
  term: '#cce8c8', // key terms: a pale green
  date: '#fbe0a1', // dates: amber
  name: '#cfe0f4', // people and places: a pale blue
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];

The three bands are palette entries like the ink and the accent, so a reader who opens the book in the Sandbox can retint the code from one swatch. They are pale, close to the lightness of the box tint, so the ink on them keeps a contrast of 12.6:1 or more, and the three stay apart when they meet in one line, as at "In 1517 Martin Luther" on page 3. A mark takes no room: paddingX reaches 0.12 em into the space beside the phrase, and paddingBottom takes the band under the descenders, both painted into space that is already there.

#3 · The opener: a painting over the title

script.js · lines 56–81in full code
const opener = {
  enabled: true,
  minHeight: mm(122), // the floor of the opener: the key starts on the same line
  marginBottom: pt(0),
  slot: {
    elements: [
      { kind: 'image', id: 'press', resourceId: 'press', fit: 'cover',
        placement: { anchor: { to: 'page', edge: 'top-left' },
          size: { width: mm(170), height: mm(96) } } },
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: SANS,
        fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
        textTransform: 'uppercase', color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(82) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF,
        fontSize: pt(26), lineHeight: 1.08, fontWeight: 700, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(2) },
          size: { width: mm(126), height: 'auto' } } },
      { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: SERIF,
        italic: true, fontSize: pt(10.5), lineHeight: LEAD / 10.5, color: col('ink'),
        align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(118), height: 'auto' } } },
    ],
  },
};

The first-level heading prints a design. The painting is anchored to the page, so it bleeds off the top and both sides, 170 by 96 mm, with fit: 'cover'. The kicker and the standfirst come from attributes on the heading line (heading attributes), and the key follows as an ordinary :::callout whose list items open on the three marks. minHeight sets the depth: the key starts on the same line however long the title runs.

#4 · Running heads by parity

script.js · lines 85–106in full code
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: SANS, fontSize: pt(7.5), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } },
  ...extra,
});
const folio = { fontWeight: 700, color: col('ink') };
const header = {
  elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ],
};
const footer = {
  elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
    ...folio, pages: 'opener',
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: mm(0), y: mm(9) } },
  })],
};

The verso carries the title of the series and the recto the title of the reading, both filtered by parity and kept off the opener by pages: 'body' (text elements). The opener carries its folio at the foot.

The whole recipe

Sandbox
// ═══ Postext Cookbook · Nº 180 · Colour-coded highlights in a study reader ════════
// https://postext.dev/en/cookbook/colour-coded-study-highlights
// Code: MIT · Text: original (CC BY 4.0) · Picture: painting made for the recipe
// Fonts: Source Serif 4, Source Sans 3 (SIL OFL 1.1) · Needs postext ≥ 1.65.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, defaultResourceTypes,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'colour-coded-study-highlights';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: semantic colours, three of them for the highlighter
const palette = {
  ink: '#1f1c19', // text: a warm near-black
  accent: '#8a3220', // kickers, rules, numbers: an iron-gall red
  tint: '#f5f0e7', // the key's background
  rule: '#d3cabc', // hairlines
  muted: '#6b645b', // running heads, colophon
  paper: '#ffffff',
  term: '#cce8c8', // key terms: a pale green
  date: '#fbe0a1', // dates: amber
  name: '#cfe0f4', // people and places: a pale blue
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
// #endregion
const OUTER = 20; // outer margin in mm: the running heads align to it
const LEAD = 14.5; // body leading in pt
const SANS = 'Source Sans 3';
const SERIF = 'Source Serif 4';

// #region answer: three highlight styles, one per kind of word, linked to the palette
// :mark[Mainz]{style="name"} in the text picks a style by id. The band is painted under
// words that stay ordinary text, so the lines break, hyphenate and justify as if the
// mark were not there, and a mark that runs over a line end gets a band on each line.
const band = (id, name) => ({
  id, name,
  background: col(id), // pale enough for ink at full contrast on top
  color: col('ink'), // the words keep the colour of the text
  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
  paddingX: em(0.12), // the band reaches a little past the first and the last letter
  paddingTop: em(0.04), paddingBottom: em(0.1), // and down past the descenders
});
const markStyles = [

The composed script.js runs as it is: paste it into any page’s module script, or open the recipe on CodePen. Recipe folder on GitHub ↗ (opens in a new tab)

Variations

#Round the ends of the bands

A radius of a fifth of the text size gives the soft ends of a felt-tip pen.

-  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
+  borderRadius: em(0.2), // soft ends, as a felt-tip pen leaves them

#Give each line its own band

With decorationBreak: 'clone', a mark that runs over a line end gets the padding at both ends of each piece, as on page 3, where "pamphlets" breaks after "pam-".

   paddingX: em(0.12), // the band reaches a little past the first and the last letter
+  decorationBreak: 'clone', // each line piece gets a whole band

Pitfalls

  • Colour alone does not survive a black-and-white photocopy, which is how many readers reach a classroom. The glossary also sets the terms in bold; if your readers will copy the pages, give one of the three kinds a second sign of its own.
  • A style id that markStyles does not declare falls back to the first style, with no error on the page (the Sandbox flags it). Keep the ids short and check the spelling in the text.

Credits

Text
  • The reading, its key, the review questions and the colophon · Ignacio Ferro · CC BY 4.0
Images
  • The printing workshop on the opening page, a painting · Generated With Diffusion Models · original
Fonts
Source Serif 4 (SIL OFL 1.1) · Source Sans 3 (SIL OFL 1.1)
Sandbox