Characters Are Just Sets of Arms
February 2026
A quick accessibility note: Screen readers hate these diagrams. A grid of│ and ─ will be read aloud as "box drawings light vertical, box drawings light horizontal..." over and over. Always wrap text diagrams in <div aria-hidden="true"> to be polite!
You've probably seen diagrams like the one above pasted into READMEs, code comments, and documentation. They look effortless — just characters on a screen. But behind every neat junction where lines meet, there's a small, elegant system at work.
We can build that system from scratch, one piece at a time. By the end, you'll see how to merge characters programmatically — and the algorithm running live.
Before pixels, we had hardware text mode
If you trace these characters back to their origin, you end up in 1981 with the release of the original IBM PC. At the time, rendering arbitrary pixel graphics was incredibly expensive. Instead, the video hardware (like the MDA and CGA cards) operated in "text mode" — an 80×25 grid where the video chip stamped out characters from a hardcoded ROM font.
The original IBM PC ROM font (Code Page 437) included 40 box-drawing characters, alongside smiling faces☺ and card suits ♠.
If you wanted to build a user interface like MS-DOS Editor or Norton Commander, you couldn't draw a window border with a graphics API. You had to use the characters burned into the hardware. So IBM engineers dedicated a huge chunk of their limited 256-character set specifically to box-drawing characters.
When Unicode came along later to unify all the world's text encodings, it absorbed these characters. They still live today in the U+2500—U+257F block, carrying the legacy of 1980s text-mode UIs into modern terminals and web browsers.
Everything is a grid
The first thing to understand is that every text diagram is just characters placed in a monospace grid. Each cell holds exactly one character. Click some cells below to paint them:
Every character takes exactly one cell. That constraint is what makes the system work.That's all a diagram is — characters in a grid. The trick is choosing the right character for each cell.
Meet the box-drawing characters
Unicode includes a block of characters specifically designed for drawing boxes and lines. Each character is defined by which arms extend from its center — up, down, left, or right.
Click any character below to see its arms:
These 11 characters live in the Unicode box-drawing block, U+2500–U+257F.
There are only four possible directions. A horizontal line ─ has left
and right arms. A vertical line │ has up and down. A corner
like ┌ has right and down. And a crossing ┼ has all four.
Drawing a single box
A box is straightforward. Each corner gets the two arms that point along its edges.
The top-left corner ┌ needs arms going right and down.
The top edge ─ needs left and right. And so on:
One box is easy. The problems start when you add a second one.
The problem: two boxes touching
When two boxes share an edge, their border characters overlap. If you just draw one box on top of the other, the second box's characters overwrite the first — and you get broken joints.
Toggle between the two approaches to see the difference:
Look at the highlighted cells where the two boxes meet. In naive mode, the second box's
characters overwrite the first — creating visible breaks. Switch to smart mode
and the overlapping characters are merged into proper junctions like ┼
that connect everything seamlessly.
How does the merging work? If an existing cell has a line going Left, and the new box brings a line going Down, the merged cell needs to support both Left and Down. We need a fast way to combine these properties without writing hundreds of if/else statements.
Characters are just sets of arms
We can represent each character as a 4-bit number. Each bit corresponds to one arm:
So ─ (left + right) is 1100, and │ (up + down)
is 0011. Click the arms below to build any character:
With 4 bits, there are 16 possible combinations — from 0000 (no arms, empty)
through 1111 (all arms, ┼). Each combination maps to exactly one
box-drawing character.
Merging = union of arms
When two box-drawing characters overlap in the same cell, we OR their arm bits together and look up the resulting character.
The algorithm:merged = lookup(arms_a | arms_b)
Try it. Pick any two characters and see the merge result:
Try picking two corners — the result is always the correct junction character.That's the entire algorithm. Five lines of code:
function merge(existing, incoming) {
let armsA = charToArms(existing);
let armsB = charToArms(incoming);
let merged = armsA | armsB;
return armsToChar(merged);
}
The lookup table itself is beautifully simple. It just maps the integer result of the OR operation back to the correct Unicode character:
const LIGHT_BOXES = {
0b0000: ' ',
0b1100: '─', // Left + Right
0b0011: '│', // Up + Down
0b1010: '┌', // Right + Down
// ...
};
When ─ (left+right) meets │ (up+down), the OR produces
all four arms — which is ┼. When ┌ (right+down) meets
─ (left+right), you get right+down+left = ┬. Every junction
"just works".
Going deeper: mixing weights
What happens when a heavy line intersects a light line? Unicode actually has specific characters for these mixed intersections (e.g., ┽, ╂, ╟).
To support this, 1 bit per arm isn't enough. We need 2 bits per arm.
00 = no arm, 01 = light arm, 10 = heavy arm.
This expands our elegant 4-bit integer into an 8-bit integer. Watch what happens to the bits when we expand our storage and upgrade our horizontal line from light to heavy:
With 8 bits, the lookup table grows from 16 entries to 81 (34 combinations). But the merge(a, b) function doesn't change at all — it's still just a bitwise union.
Five styles, one algorithm
The same arm-union trick works across every box-drawing style. Each style is just a different lookup table mapping the same 4-bit arm patterns to different characters.
Click a style to see the same diagram re-rendered:
The font metric trap
In CSS,line-height: 1.5 destroys ASCII boxes because it adds vertical space between rows that the font wasn't designed to fill.
If you try to build a diagram editor yourself, you'll immediately hit a frustrating wall: your vertical lines will have 1-pixel gaps between them.
Box-drawing characters are designed to tile perfectly, extending all the way to the absolute edges of their bounding box. But web browsers and modern text editors inject line height, or use text-rendering algorithms that prioritize legibility over perfect tiling.
To get perfectly seamless lines in the interactive canvas below, we can't just use standard text measurements. We have to query the font's actual bounding box (using fontBoundingBoxAscent and fontBoundingBoxDescent) to size our grid cells, ensuring every character tiles without a single pixel of empty space.
Try it yourself
Here's a miniature version of the wireframe editor from niji.sh. Draw boxes by clicking and dragging. Watch the borders merge automatically when boxes touch. Hit "Copy" to grab the text output.
That's it
Every neat T-junction and crossing you've ever seen in an ASCII diagram comes down to the same idea: characters are sets of arms, and merging is a bitwise OR.
The full wireframe editor at niji.sh/wireframe uses this exact algorithm to let you draw boxes, tables, lines, and arrows — all merging seamlessly — and export the result as copyable text.