Vertical layout metrics — verticalFlowMetrics

Reads out the values the engine actually used for a vertical
(writing-mode: vertical-rl) multi-column block, from page JavaScript.
You get column boundaries, stride, line count, and how much each line really
occupied — without inferring anything from coordinates.

Ebook viewers use this to answer "which line are we on" and "where does the
next column start". Previously the only option was to line up
getBoundingClientRect() results and guess, which stopped matching as soon as
hanging punctuation or a column break was involved.


Using it

const m = window.__blinkgtk?.verticalFlowMetrics(document.getElementById('book'));
if (m) {
  console.log(m.columnCount, m.stride, m.originX);
}

The argument is the vertical multi-column container element. When it does
not apply (not a vertical multi-column block, layout not yet resolved, and so
on) it returns null rather than throwing, so you do not need try/catch.

Enabling it

This API is not present by default. It requires a startup flag:

--enable-blink-features=CJKVerticalColumnFragmentation

Without the flag, window.__blinkgtk itself does not exist. Feature-detect
with ?. or typeof, as above; calling it blindly where it is absent raises a
TypeError.

--enable-experimental-web-platform-features enables it as well. That switch turns
on every feature with status: "experimental", and CJKVerticalColumnFragmentation
is one of them (checked on 2026-09-27 with the Linux build on Chromium 154:
window.__blinkgtk is present).

The same feature also switches the page layout of vertical multi-column content
(four places in the layout code branch on it). An application that passes
--enable-experimental-web-platform-features for another reason may therefore see
vertical multi-column text laid out differently. We have not measured the layout
change itself under that switch.

What you get

All values are in content px — layout coordinates, before page zoom.

Name Type Meaning
stride number Advance per column: used column width + used column gap
originX number Right edge X of the first line (start of the rightmost column)
columnCount number Number of columns
lineCount number Total number of body lines
contentWidth number Total block-direction size of the multi-column block
hasBoundaries boolean Whether boundaries can be derived (stride > 0 and columnCount > 0)
boundaries null See below. Currently always null
generation number Layout generation. Unchanged means no recomputation is needed
lines Array Per-line measurements (below)

Column boundaries

boundaries is currently always null. CSS multi-column layout is
structurally evenly spaced (same used column width, same used gap), so an array
would carry no extra information. Derive boundaries by arithmetic:

// Right edge X of column n (0-based, from the right)
const rightEdge = n => m.originX - m.stride * n;

If a form with uneven spacing (for example column-span) ever needs to be
supported, a measured array will be placed here then. Check hasBoundaries
before relying on it.

Each entry of lines

Name Type Meaning
inlineSize number What the line actually occupied — not the space available to it
blockOffset number Block-direction position of the line (X in vertical writing)
columnIndex number Which column the line belongs to. The engine's own value, not a coordinate comparison
hasHanging boolean Whether the line ends with hanging punctuation. The amount is not retained, only the flag

The point is that inlineSize is what was occupied. Comparing it against
contentWidth tells you whether a line filled its column.

Real values

Measured against the shipped build (captured 2026-09-03, Chromium 152.0.7977.64): a 900x640 window,
column-width: 11em, font: 16px/1.75 serif, column-gap: 2em.

These figures were captured on Chromium 152 and have not been re-measured on 153 (BlinkGTK 1.2.2) or 154 (BlinkGTK 1.2.3). The line-end handling added in 153 (LineBreakerHanKerningEnd) changes how many characters fit on a line, whereas the figures here are column dimensions (stride, origin, count) - a different quantity. Even so, we have not checked, so we cannot claim the values are unchanged. Read the actual values at run time through the API below.

{
  "stride": 272,
  "originX": 352,
  "columnCount": 2,
  "lineCount": 24,
  "contentWidth": 544,
  "hasBoundaries": true,
  "boundaries": null,
  "generation": 2,
  "lines": [
    { "blockOffset": 324, "columnIndex": 0, "hasHanging": false, "inlineSize": 224 },
    { "blockOffset": 296, "columnIndex": 0, "hasHanging": false, "inlineSize": 240 }
  ]
}

Reading it: two columns, stride 272px. The rightmost column starts at X=352, so
the second is at 352 - 272 = 80. There are 24 lines; the first two are both in
column 0. Line 1 occupied 224px, line 2 occupied 240px.

Things to watch

See also