Skip to content

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.

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.

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.

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));
}
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]) -> '▁▃▂▆▄█▅'
  • 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
  • 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 empty cells 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.