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.
--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.
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.
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.
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()