verticalFlowMetricsReads 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.
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.
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.
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) |
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.
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.
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.
{
"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.
null. Wait a frame after load before
calling itgeneration to skip needless work.
If it has not changed, the layoutwindow.devicePixelRatioblink_web_view_execute_javascript()