Thirteen Rulers, One Column
A terminal is a grid of cells, and every aligned thing you have ever seen in one depends on two separate systems agreeing about how many cells a character takes. Neither system is in charge. Here are twelve shipped width tables and one running terminal, asked about every assigned Unicode character, and then the fonts, asked whether they can draw what the tables reserved.
Loading the measurements…
Type into the box. Each row lays your text out on a column grid the way one width authority would, and the number on the right is how many columns it thinks you used. They are all reading the same string.
| codepoint | name | columns |
|---|
Two systems, no referee
A terminal draws text by reserving a whole number of fixed-width cells for each character and then painting a glyph into them. The reservation is arithmetic: a lookup in a width table that returns 0, 1 or 2. The painting is typography: a font supplies a glyph with an advance width measured in units of an em square. Alignment is the claim that those two numbers match.
Nothing checks that claim. The width table is chosen by the program printing the text, or by the terminal, or by the multiplexer between them, and there are usually several in the stack disagreeing quietly. The font is chosen by the reader, and when the reader's font has no glyph for a character the system substitutes another font that never agreed to anything. This page measures both halves.
Every table, every character
The panel is twelve width tables plus one running terminal. Two of the twelve are the same specification recipe resolved the two legal ways, and two more are the same Go library with one boolean flipped, which is itself the finding: the disagreement starts before you have left a single source. The other nine are the defaults people actually run.
Asked about all 0 assigned scalar values that are not surrogates, private use, or control characters, the twelve tables agree on 0 of them and split on 0, which is 0 of the assigned characters in Unicode.
That headline number is inflated by tables nobody would defend as current, so the same question gets asked of stricter and stricter panels. The last row is the one that matters: six implementations that all claim a recent Unicode, judged only on characters old enough that none of them can plead ignorance.
| panel | tables | characters | they split on |
|---|
So the floor is 0 characters out of 0, or 0, on which six current implementations disagree with nothing to blame it on. That is small. It is also not zero, and the next section is about which characters are in it.
Why they split
| characters | cause |
|---|
Those five buckets partition the 0 contested characters, in that order, so each character is counted once. Only the first is unfixable: for the 0 characters whose East Asian Width property is Ambiguous, Unicode Standard Annex #11 says in as many words that the width depends on the context the text is displayed in. Both readings are conformant. No amount of care inside any single program settles it, because the question was never given an answer to find.
The characters that exist only to be lined up
Box Drawing is a Unicode block of 128 characters whose entire purpose is to be laid out in a grid. Corners, tees, crossings, the light and heavy and double rules that every command line tool draws its tables with.
0 of those 128 have no agreed width, and every one of those is in the Ambiguous class. The characters invented so that terminals could draw aligned boxes are, by the standard's own definition, of undefined column width. Green below is agreed, red is disputed; click any character to load it into the bench.
| block | range | assigned | disputed | Ambiguous class |
|---|
Braille Patterns is the one clean block, all 256 agreed at one column, which is why the spinner in your package manager is usually made of Braille dots. It is the only graphical block in this list that every width table in the panel treats identically. That is not an accident of taste; it is a block that arrived after the Ambiguous mess and was given a definite width.
What year is each table living in
A width table is generated once from a Unicode release and then it ages. You can see how much without being told: for each Unicode release, ask what fraction of the characters that release introduced this table still gets right, and read the row. Green is agreement with the current specification, red is not. The Ambiguous class is excluded, because resolving it the other way is a choice rather than an age.
Some tables fall off a cliff, which is what a table generated once and left alone looks like from outside:
A cliff here means the last release where the table matched the specification on more than 90% of that release's new characters, followed immediately by one where it matched on under 10%, counting only releases that introduced at least fifty characters. The tables without a cliff are not necessarily current; several are simply ragged.
One package, one million characters, one wrong unit
The npm package wcwidth is a JavaScript port of Markus Kuhn's C reference implementation, the ancestor of most of the tables above. It was downloaded 50,020,020 times in the week ending 29 August 2026.
It reports every single one of the 1,048,576 codepoints above U+FFFF as two columns wide. Not the East Asian ones. All of them. Deseret, Linear B, Gothic, Old Italic, the mathematical alphanumerics, the playing cards, every emoji and every character in every script Unicode has added to the supplementary planes since 1999.
The cause is visible in eight lines of its own source. Its string function loops over str.charCodeAt(i), which walks UTF-16 code units, not characters. An astral character arrives as a surrogate pair, each half falls through the table to the default of 1, and the sum is 2. It is not a width table saying "wide". It is a string length in the wrong unit wearing a width table's name. In the census above this accounts for 0 of the contested characters, filed separately from real disagreement because it is not one.
The terminal's answer is its C library's answer
The thirteenth row of the bench is not a table. Each character was written to the tty of a live tmux pane and the cursor column read back, so it reports what a terminal did rather than what a library thinks. That makes it possible to ask a question the tables cannot answer about themselves: where does a running terminal get its widths?
Compare its readings to each table, over only the characters that table will answer about at all.
| table | characters it will answer about | the terminal differed on |
|---|
One row is not like the others. Over the 0 characters this machine's C library has an opinion about, the terminal gave a different answer 0 times. Not thousands. Here they all are, and they are all pictographs that the terminal widens to two columns and the C library still calls one.
| codepoint | name | glibc | the terminal |
|---|
Which leaves the characters the C library has never heard of, because it was built against an older Unicode. There are 0 of those in the probed set, and the terminal gave every one of them the same answer: 1 column. For 0 of them the current specification says two. Those are not exotic: 0 of them are characters Unicode 0 added as Wide, which this terminal is quietly giving one column each.
So the width of your terminal is a property of your C library, not of your terminal. Upgrading the terminal will not move these numbers; upgrading libc will. That is not written down anywhere as far as this page's author could find, and it is measurable in about a minute with the script in the research directory.
And a tab is not a width
The probe wrote the control characters too. Sixty-four of the sixty-five left the cursor in column zero. The exception is U+0009, which moved it eight columns, because a tab is not a character with a width, it is an instruction to go somewhere. Every table in the panel answers 0 or refuses. The terminal answers 8, and the terminal is right, and the question was wrong. This is why the census excludes the control characters from the width comparison.
What a table cannot answer at all
Every table above answers about one codepoint. People do not type codepoints. A family emoji is seven codepoints and one character; a flag is a base and five invisible tag characters; a skin-toned waving hand is two. Unicode publishes a list of the 0 fully-qualified emoji sequences it recommends for general interchange, and every one of them is meant to occupy two columns.
Summing a per-codepoint table over a sequence gets this wrong most of the time, which is why the good libraries stopped summing. Below, the four instruments that take a whole string rather than a character, including a real terminal that had to decide rather than compute.
| instrument | gave 2 columns | of | what it gave instead |
|---|
tmux 3.4, asked by writing each sequence into a live pane and reading the cursor column back, moved the cursor by something other than two columns on 0 of them, which is 0 of the emoji Unicode currently recommends. Here are 0 of them, the number after each being the columns the terminal actually moved. Click one to put it on the bench.
The other half: the fonts were never asked
Suppose every table above agreed. The terminal reserves two cells for an ideograph, and now a font has to fill them. Two cells is twice the advance width of the letter A in whatever monospace font the reader chose. An ideograph, in every CJK font measured here and as drawn by the browser, is exactly one em.
Those two quantities have no reason to be equal, and mostly are not.
| monospace font | two cells, in ems | an ideograph fills | an emoji fills |
|---|
Only a font whose cell is exactly half an em makes two cells exactly one em. Of the 0 faces in this table that anyone actually sets a terminal to, exactly 0 is: . In every other one, an ideograph is narrower than the space reserved for it and an emoji is wider, in the same terminal, on the same line, with the same width table saying 2 for both. The two faces at the bottom of the table that do hit one em exactly are GNU Unifont, which was designed as a dual-width grid font, and a CJK face, which nobody codes in.
It gets worse when you ask what the monospace font actually contains. These are the font files read directly: the advance width per glyph out of hmtx, the character-to-glyph map out of cmap, one cell defined as the advance of U+0041.
| font | cell, in ems | claims fixed pitch | characters | at two cells | off grid | box drawing | braille |
|---|
All 0 of the terminal faces have not one single glyph two cells wide. They do not contain the characters the width tables have been arguing about. When one appears, the system goes and finds it in another font, and that font's glyph is whatever width it happens to be.
Two smaller things fall out of the same table. contain none of the 128 box drawing characters at all, and contain well under half, so the aligned tables they appear to draw are drawn by a substitute font. And 0 of them, , report isFixedPitch as false in their post table, which is a monospace font declaring in its own metadata that it is not one.
Your machine, not this one
The fonts above are the ones this study had. Yours are different, so here is the same measurement made in your browser, just now, on the stacks your system may or may not resolve. A cell is the advance of A; the other columns are how many cells your browser gives each character.
| font stack requested | cell, px at 100px | ─ | 一 | 😀 |
|---|
The check
- Every table in the panel was run, not read. glibc and musl were asked by compiling the same twenty-line C file against each library and calling wcwidth on all 1,112,064 scalar values. utf8proc was asked through its own C API. Python and the two npm packages were asked through their public functions. xterm.js was asked inside a real headless browser, on the provider object VS Code installs.
- The terminal row is a measurement, not a lookup. Each character was written to a live tmux pane's tty and the cursor column read back through display-message. The rig carries known-answer controls (A must read 1, an ideograph 2, a combining accent 0) and refuses to run if they fail. They caught two real bugs during construction: a read/parse race that made an ideograph read as one column, and bash's printf '\U%08X' silently emitting six ASCII bytes instead of a character, which made it read as six.
- The two routes to xterm.js's Unicode 11 table, one in Node and one in the browser, produce byte-identical files. If they had not, both would be suspect.
- The census runs on assigned, non-surrogate, non-private-use, non-control characters. Controls are excluded because comparing wcwidth's -1 to utf8proc's 0 compares two contracts, not two widths. Private use is excluded because a width there is a guess by construction. Both exclusions are counted and reported rather than hidden.
- Every number on this page is recomputed in your browser from the four JSON files in this directory, which are written by the probes in research/display-width/. The offline verifier recomputes the load-bearing ones from the raw probe outputs by a different route.
What this does not show
- One machine, one day. Every version here is the one this container had on 2 September 2026: glibc 2.39, musl 1.2.4, utf8proc 2.9.0, Python wcwidth 0.8.3, npm wcwidth 1.0.1, string-width 8.2.2, xterm.js 6, go-runewidth 0.0.29, tmux 3.4, against Unicode 17.0.0. Newer versions will move these numbers, and that is the point rather than a flaw.
- The font half is about advance widths, not rendering. Some terminals scale or clip a glyph to fit the cells they reserved; others let it overhang. This page measures what the font offers and what a browser draws, not what every terminal emulator then does about it.
- The emoji advance has a small instrument gap. The font file gives Noto Color Emoji an advance of 2550 units on a 2048 unit em, or 1.2451 em. Chromium, asked to draw the same glyph, gives 1.2500 em when the font is named directly and 1.2477 em when it arrives through fallback. The three readings agree to within half a percent and all three exceed two cells in every mainstream monospace font; which of them is exactly right for a colour bitmap font is not settled here.
- tmux was not asked about everything. Its probe covered the 159,866 assigned non-private-use characters, control characters included. Unassigned and private use characters were left out because the answer there would be a default rather than a decision, and the bench says "not asked" for them rather than guessing.
- The terminal probe measures the column, not the position. A line feed reads as zero columns because the cursor went down a row and came back to column zero. Vertical movement is outside what this instrument can see.
- A width table is not the only thing that can go wrong. Terminals also differ on what happens at the right margin, on whether a wide character can straddle it, and on how a combining mark attaches. None of that is measured here.
The shape of it
The received account of this problem is that emoji broke terminals. The measurement says something narrower and stranger. The characters Unicode invented for drawing aligned grids were given a width property that explicitly means "it depends". The libraries that resolve it are internally consistent and mutually incompatible, and several of them ship a switch so you can pick. The one reference implementation most of them descend from, ported to JavaScript, silently swapped the unit of measurement. And underneath all of it, the fonts that were supposed to fill the cells do not contain the characters in question at all, so the glyph that arrives comes from a font that never agreed to the cell size, and misses it by anything from a fifth too narrow to a quarter too wide.
An aligned terminal is not a solved problem with some edge cases. It is a coincidence that holds for ASCII, and everything past ASCII is two independent systems being asked to agree with no mechanism for doing so.