Learning Highlight Api

 from Red Blob Games
10 Apr 2026

I want to learn the Custom Highlight API[1], newly available in browsers as of June 2025. On some of my pages, my code samples have HTML in them (for interactive elements). That means I can't use a standard syntax highlighter that replaces the HTML. Instead, I am hoping to use the Custom Highlight API combined with the Mutation Observer API.

For testing, I used some parts of the hexagon guide:

var {{variant.replace('_', '')}}_direction_differences = [
    // {{['even','odd'][parity]}} {{parityField}}s
    [[{{neighbor(hex, parity)}}]],
]

function {{variant.replace('_', '')}}_offset_neighbor(hex, direction):
    var parity = hex.{{parityField}} & 1
    var diff = {{variant.replace('_', '')}}_direction_differences[parity][direction]
    return OffsetCoord(hex.col + diff[0], hex.row + diff[1])


function {{output}}_to_oddr(hex):
    var parity = hex.r&1
    var col = hex.q + (hex.r - parity) / 2
    var row = hex.r
    return OffsetCoord(col, row)

function oddr_to_{{output}}(hex):
    var parity = hex.row&1
    var q = hex.col - (hex.row - parity) / 2
    var r = hex.row
    return {{output_constructor}}

function {{output}}_to_evenr(hex):
    var parity = hex.r&1
    var col = hex.q + (hex.r + parity) / 2
    var row = hex.r
    return OffsetCoord(col, row)

function evenr_to_{{output}}(hex):
    var parity = hex.row&1
    var q = hex.col - (hex.row + parity) / 2
    var r = hex.row
    return {{output_constructor}}

function {{output}}_to_oddq(hex):
    var parity = hex.q&1
    var col = hex.q
    var row = hex.r + (hex.q - parity) / 2
    return OffsetCoord(col, row)

function oddq_to_{{output}}(hex):
    var parity = hex.col&1
    var q = hex.col
    var r = hex.row - (hex.col - parity) / 2
    return {{output_constructor}}

function {{output}}_to_evenq(hex):
    var parity = hex.q&1
    var col = hex.q
    var row = hex.r + (hex.q + parity) / 2
    return OffsetCoord(col, row)

function evenq_to_{{output}}(hex):
    var parity = hex.col&1
    var q = hex.col
    var r = hex.row - (hex.col + parity) / 2
    return {{output_constructor}}


Array<Cube> results = []
for each -N ≤ q ≤ +N:
    for each -N ≤ r ≤ +N:
      for each -N ≤ s ≤ +N:
          if q + r + s == 0:
              results.append(cube_add(center, Cube(q, r, s)))


function hex_reachable(Hex start, int movement): Set<Hex>
    Set<Hex> visited = Set()
    visited.add(start)
    Array<Array<Hex>> fringes = []
    fringes.append([start])
    
    for each 1 ≤ k ≤ movement:
      fringes.append([])
      for each hex in fringes[k-1]:
          for each 0 ≤ dir < 6:
              Hex neighbor = hex_neighbor(hex, dir)
              if neighbor not in visited and not blocked:
                  visited.add(neighbor)
                  fringes[k].append(neighbor)
    
    return visited

// note that part of this is editable -- I wanted to test that editing doesn't mess up the highlighting
var oddr_direction_differences = [
    // even rows
    [[+1,  0], [ 0, -1], [-1, -1],
    [-1,  0], , [ 0, +1]],
    // odd rows
    [[+1,  0], [+1, -1], [ 0, -1],
    [-1,  0], [ 0, +1], [+1, +1]],
]

function oddr_offset_neighbor(Hex hex, int direction): OffsetCoord
    int parity = hex.row & 1
    var diff = oddr_direction_differences[parity][direction]
    return OffsetCoord(hex.col + diff[0], hex.row + diff[1])

Syntax highlighting with prism.js or highlight.js generates new HTML. I don't want new HTML. I want the token positions. So instead of a syntax highlighting library, I really want a tokenizer (or parser):

Those sizes are without the grammar. I might want to define my own grammars anyway, especially for the pseudocode I use sometimes. I think I don't need anything nearly as fancy as those libraries, so I made my own.

Limitations:

After the initial (partial) experiments on this page, I put it into production on the Hexagon Guide page.

Source: learning-highlight-api.js and /grids/hexagons/code-highlighting.js

Email me , or comment here: