Sub-character precision with Unicode blocks
Source: kaleidoscope/src/demos/progress-bar.tsx · [kaleidoscope/src/demos/sparkline.tsx] Category: Pattern — terminal rendering
Sub-character Unicode precision — terminal output is one-cell granular, which makes bars and sparklines chunky. Unicode has enough block-element glyphs to carve each cell into 8 fractional widths (horizontal) or heights (vertical), giving you far smoother rendering without leaving plain text.
What it is
Section titled “What it is”Two families of block-element characters:
- Horizontal:
▏ ▎ ▍ ▌ ▋ ▊ ▉ █— 1/8 to 8/8 width, left-aligned. For progress bars, meter fills. - Vertical:
▁ ▂ ▃ ▄ ▅ ▆ ▇ █— 1/8 to 8/8 height, bottom-aligned. For sparklines, waveforms.
Pair with a continuous value and a width, and you have sub-pixel rendering in a character grid.
Why it exists
Section titled “Why it exists”The problem: “A 20-wide progress bar” only has 21 distinct states (0 cells, 1 cell, …, 20 cells filled). Animating progress shows jumps every 5%. Sparklines with just full blocks look blocky.
The fix: multiply your effective resolution by 8. Each cell becomes 8 sub-steps. A 20-cell bar is now 160 sub-steps — visually smooth at any reasonable animation rate.
Horizontal shape (progress bars)
Section titled “Horizontal shape (progress bars)”const H = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
function hbar(progress, width) { const sub = Math.round(progress * width * 8); const full = Math.floor(sub / 8); const partial = H[sub % 8]; const empty = width - full - (partial ? 1 : 0); return '█'.repeat(full) + partial + ' '.repeat(Math.max(0, empty));}Vertical shape (sparklines)
Section titled “Vertical shape (sparklines)”const V = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
function sparkline(values, max = Math.max(...values)) { return values.map(v => V[Math.min(7, Math.floor(v / max * 8))]).join('');}
// sparkline([1, 3, 2, 6, 4, 8, 5]) -> '▁▃▂▆▄█▅'How it’s used
Section titled “How it’s used”- Kaleidoscope — progress bars, sparklines, meter demos
- Atrium’s stats dashboard uses the horizontal version
- btop / bashtop / glances / lazygit — built-in to professional TUIs
- Pattern generalizes to any text rendering where you want finer-than-character resolution
Gotchas
Section titled “Gotchas”- Font coverage. Most programming fonts ship all 8 block variants. Some older terminal fonts skip a few, which renders as ? or a placeholder.
- Proportional fonts break the illusion. Each block glyph has a defined width but proportional fonts may render them wider. Force monospace in the surrounding CSS or terminal.
- Color bleed. Coloring a full block next to a partial block can create a seam where the background shows through. Keep the same background color consistently.
- Unicode subset matters. Some SSH clients or very minimal terminals don’t render the full range. Offer an ASCII fallback:
[==== ]style. - Bar widths: the
emptycells must render with the same width as full cells. A space character is cell-wide in monospace, which is what you want. - Halfwidth punctuation trap. Single-width characters interleaved with fullwidth (CJK) characters don’t align. For CJK contexts, use the fullwidth block variants.
See also
Section titled “See also”- components/progress-bar-subpixel — the canonical example
- components/loading-skeleton — sibling “smooth rendering in text” technique