Nº 544 of 605
ASCII art homepage
A homepage drawn in 7-bit ASCII on a monospace grid: a generated sunset with a packet on a wire, a typing command line, bracket buttons that go solid.
Preview
Use it
Free to use · credit appreciated1 Once, in your project · Node 22.20+
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "ASCII art homepage" (piece ascii-art-homepage) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 544 · "ASCII art homepage" · www.designlounge.live -->
# ASCII art homepage
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
The marketing homepage for "Fernwire", an invented sync tool for plain-text notes, drawn entirely with characters on a monospace grid. The hero illustration is a sunset over three ridges with a wire strung between two poles, generated in JS from 7-bit ASCII at whatever width the column has; a rust `o` (a note in transit) travels along the wire. Dividers, buttons (`[ Download for macOS ]`), the product diagram and the pricing frames are characters too. The page sits on warm paper, not black, so it reads as a printed manual rather than a terminal clone. The detail worth copying: **every glyph in the art, frames and buttons is in U+0020 to U+007E**, because Google Fonts' monospace subsets ship no box-drawing or block characters, and one fallback glyph breaks the grid.
## Reference behaviour
1. Initial state at 1280 × 800: a 72px nav with a dashed bottom rule; a two-column hero (46ch copy column, art column fills the rest), bottom-aligned; under it a "how it works" divider. The art is already drawn, the clock already shows local time, and the typing line is mid-type.
2. The nav clock reads `HH:MM:SS local` and updates every 1000ms.
3. The typing line in the hero types a command at 70ms per character after a `$ ` prompt, waits 420ms, prints the result on the second line after `> `, and starts the next of three commands every 6000ms: `fernwire sync ~/notes` → `1,204 files · 3 devices · 0 conflicts`; `fernwire log --today` → `14 notes changed · 2.1 KB of diffs`; `fernwire relay status` → `eu-2 healthy · holding 0 bytes`. A 1ch block cursor blinks with `steps(1)` at 1.1s.
4. The art redraws every 120ms: the packet `o` advances 2.5% of the wire per tick (one crossing every 4.8s); the caption reads `relay eu-2 · packet NN%`. Nothing else in the art moves.
5. Hover or focus a bracket button: an ink block grows from the left edge across the full label, brackets included, over 220ms; label and brackets turn paper. The brackets "go solid". The primary button is rust-filled at rest and turns ink on hover.
6. Hover a nav link: text goes from `--ink-2` to `--ink`, and a `./` prefix (already laid out, transparent) turns rust, so nothing shifts.
7. The changelog form at the foot: submit empties the field and its placeholder becomes "subscribed, thanks".
8. Resize: the art is regenerated to the column's character count; at < 640px it draws 18 rows instead of 24 and the diagram swaps to a vertical version.
## Structure
```
1280 × 800 (wrap 1184px, gutter 48px; 15px/24px Plex Mono; 1ch ≈ 9px)
+------------------------------------------------------------------------------+
| [~] fernwire how it works pricing changelog manual 20:37:03 local [ sign in ] | 72
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| |
| v3.2 · sync for plain-text notes * . . |
| Notes that -+- @@@@@ -+- |
| stay .txt (72px, 600) |__ @@@@@@@@@ __| |
| lede 44ch | ---___o___---- | | art 13/15
| +- $ fernwire sync ~/notes_ ---+ | ########## | | 24 rows
| | > 1,204 files · 3 devices | ....::::::::::::::.......... | |
| +------------------------------+ ::::::::::::::::::::::::::::| |
| [ Download for macOS ] [ Read the manual ] ////////////////////////////// |
| fig.1 a note in transit relay eu-2 · packet 50% |
| how it works - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| +-------+ +-------+ +-------+ | what it does, plainly |
… 173 more lines. Copy or download for the full brief. Loading the source…Brief
213 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
The marketing homepage for “Fernwire”, an invented sync tool for plain-text notes, drawn entirely with characters on a monospace grid. The hero illustration is a sunset over three ridges with a wire strung between two poles, generated in JS from 7-bit ASCII at whatever width the column has; a rust o (a note in transit) travels along the wire. Dividers, buttons ([ Download for macOS ]), the product diagram and the pricing frames are characters too. The page sits on warm paper, not black, so it reads as a printed manual rather than a terminal clone. The detail worth copying: every glyph in the art, frames and buttons is in U+0020 to U+007E, because Google Fonts’ monospace subsets ship no box-drawing or block characters, and one fallback glyph breaks the grid.
Reference behaviour
- Initial state at 1280 × 800: a 72px nav with a dashed bottom rule; a two-column hero (46ch copy column, art column fills the rest), bottom-aligned; under it a “how it works” divider. The art is already drawn, the clock already shows local time, and the typing line is mid-type.
- The nav clock reads
HH:MM:SS localand updates every 1000ms. - The typing line in the hero types a command at 70ms per character after a
$prompt, waits 420ms, prints the result on the second line after>, and starts the next of three commands every 6000ms:fernwire sync ~/notes→1,204 files · 3 devices · 0 conflicts;fernwire log --today→14 notes changed · 2.1 KB of diffs;fernwire relay status→eu-2 healthy · holding 0 bytes. A 1ch block cursor blinks withsteps(1)at 1.1s. - The art redraws every 120ms: the packet
oadvances 2.5% of the wire per tick (one crossing every 4.8s); the caption readsrelay eu-2 · packet NN%. Nothing else in the art moves. - Hover or focus a bracket button: an ink block grows from the left edge across the full label, brackets included, over 220ms; label and brackets turn paper. The brackets “go solid”. The primary button is rust-filled at rest and turns ink on hover.
- Hover a nav link: text goes from
--ink-2to--ink, and a./prefix (already laid out, transparent) turns rust, so nothing shifts. - The changelog form at the foot: submit empties the field and its placeholder becomes “subscribed, thanks”.
- Resize: the art is regenerated to the column’s character count; at < 640px it draws 18 rows instead of 24 and the diagram swaps to a vertical version.
Structure
1280 × 800 (wrap 1184px, gutter 48px; 15px/24px Plex Mono; 1ch ≈ 9px)
+------------------------------------------------------------------------------+
| [~] fernwire how it works pricing changelog manual 20:37:03 local [ sign in ] | 72
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| |
| v3.2 · sync for plain-text notes * . . |
| Notes that -+- @@@@@ -+- |
| stay .txt (72px, 600) |__ @@@@@@@@@ __| |
| lede 44ch | ---___o___---- | | art 13/15
| +- $ fernwire sync ~/notes_ ---+ | ########## | | 24 rows
| | > 1,204 files · 3 devices | ....::::::::::::::.......... | |
| +------------------------------+ ::::::::::::::::::::::::::::| |
| [ Download for macOS ] [ Read the manual ] ////////////////////////////// |
| fig.1 a note in transit relay eu-2 · packet 50% |
| how it works - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| +-------+ +-------+ +-------+ | what it does, plainly |
| |laptop |<=====>| relay |<=====>| phone | | [x] Syncs any folder ... |
+------------------------------------------------------------------------------+
below the fold: pricing (3 framed plans) · footer with changelog email field
<header class="nav">: logo link ([~]in rust + “fernwire” 600),<nav><ul>of four links,<span class="clock">, a.btn“sign in”.<main>:<section class="hero">: gridminmax(0,46ch) minmax(0,1fr), gap 4ch,align-items: end, padding56px 0 24px. Left:.eyebrow,<h1>,.lede,.type(two lines),.ctas. Right:<figure>holding<pre class="art" role="img" aria-label>and a<figcaption class="cap">with two spans..divdividers: a bold label then an::afterstring of-repeated, clipped withoverflow: hidden.<section class="how">: gridminmax(0,1fr) 40ch. Left:<pre class="diag h">(horizontal, 77 columns) and<pre class="diag v" aria-hidden>(vertical, 21 columns), then a note. Right:<table class="spec">with[x]/[ ]rows.<section class="plans">: three<article class="plan">with an<h3>that sits on the top border ([ plus ]), a 32px price,+list items. The middle plan is inverted (ink fill).
<footer class="foot">: copyright line and a<form class="sub">with labelchangelog >and an email input.
Tokens
:root {
/* colour: warm paper, one rust accent */
--paper: #f1ece1; /* page */
--paper-2: #e8e1d2; /* typing-line well */
--ink: #1c1a16; /* text, frames, button fill on hover, hot plan */
--ink-2: #544e44; /* nav links, captions, far ridges, stars (7.0:1 on paper) */
--rule: #8a8172; /* dashed rules, divider dashes, well border */
--rust: #a8391b; /* accent: sun, packet, brackets, primary button (5.5:1 on paper) */
--rust-ink: #fff6ea; /* text on rust */
--rust-soft: #f0a184; /* list markers on the ink plan */
/* type: one family */
--mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
--fs: 15px; --lh: 24px; /* body */
--fs-h1: clamp(36px, 5.6vw, 72px);
--fs-price: 32px;
--fs-diag: 14px; --lh-diag: 20px;
--art-fs: 13px; --art-lh: 15px; /* art grid */
--fs-cap: 13px;
/* layout */
--wrap: 1184px; --gut: 48px;
--nav-h: 72px;
--hero-cols: minmax(0, 46ch) minmax(0, 1fr);
--btn-h: 40px;
--radius: 0;
/* motion */
--t-micro: 140ms;
--t-fill: 220ms;
--type-step: 70ms; --type-pause: 420ms; --type-cycle: 6000ms;
--art-tick: 120ms; --packet-step: 0.025;
--blink: 1.1s;
--ease: cubic-bezier(.2, .7, .2, 1);
/* focus */
--focus: 2px dashed var(--rust); --focus-offset: 3px;
}
Typography
| Role | Family | Size / line | Weight | Tracking | Colour |
|---|---|---|---|---|---|
| H1 “Notes that stay .txt” | IBM Plex Mono | clamp(36px, 5.6vw, 72px) / 1.02 | 600 | -0.035em | --ink, .txt in --rust |
| Body, lede, nav | IBM Plex Mono | 15 / 24 | 400 | 0 | --ink / --ink-2 |
| Logo | IBM Plex Mono | 15 / 24 | 600 | 0 | [~] rust |
| Buttons | IBM Plex Mono | 15 / 24 | 500 | 0 | brackets rust |
| Art | IBM Plex Mono | 13 / 15 | 400 (packet 600) | 0 | sun rust, far ridges and stars --ink-2, near ridge and poles --ink |
| Diagram | IBM Plex Mono | 14 / 20 | 400 (labels 600) | 0 | “diffs” rust |
| Price | IBM Plex Mono | 32 / 40 | 600 | -0.02em | |
| Caption | IBM Plex Mono | 13 / 24 | 400 | 0 | --ink-2 |
Set font-variant-ligatures: none on body. Plex Mono ligates nothing by default, but swapping the family for one that does (<=>, ->) breaks the diagram.
Motion
| Thing | Trigger | Property | From → to | Duration / easing | Reduced motion |
|---|---|---|---|---|---|
| Clock | load, every 1000ms | text | time | instant | still updates (it is content) |
| Typing line | load, every 6000ms | text, one char per tick | "" → command | 70ms per char, result after 420ms | full command and result, no cycling |
| Cursor | always | background | ink ↔ transparent | 1.1s steps(1) | static |
| Packet on wire | every 120ms | redraw art | t → t + 0.025 | stepwise | art drawn once, packet at 0% |
| Button fill | hover / focus | ::after transform: scaleX | 0 → 1, origin left | 220ms --ease | 1ms |
| Bracket colour | hover / focus | color | rust → paper | 140ms --ease | 1ms |
Nav ./ prefix | hover | color | transparent → rust | 140ms --ease | 1ms |
| Button press | active | transform | 0 → translateY(1px) | instant | same |
States
- Bracket button rest: no fill, label
--ink, brackets--rust, height 40px, no border. - Hover / focus-visible: ink block fills left to right inset 6px top and bottom (so it hugs the text line), label and brackets become
--paper. - Primary rest: rust block (
--rust), text and brackets--rust-ink. Hover: block becomes--ink. - Active:
translateY(1px). - Focus-visible (all links, inputs):
2px dashed --rust, offset 3px. - Nav link hover:
--ink,./prefix appears in rust (it is always laid out as transparent text). - Spec table:
[x]rows are what the product does,[ ]rows what it never does; both markers in rust. - Plan “plus”: inverted, ink fill, paper text, title tab on rust.
- Email field: 1px
--ink-2border, 2px--inkbottom; placeholder--ink-2; after submit the placeholder reads “subscribed, thanks”.
Accessibility
- The art is
<pre role="img">with anaria-labelthat describes the drawing; screen readers do not read 1,700 punctuation marks. The vertical diagram duplicate isaria-hidden; the horizontal one hasrole="img"and a label that states what the diagram says. - The typing line is
aria-live="off": it repeats, so announcing it would be noise. The same facts are in the lede and diagram. - The clock has
aria-label="Local time". - Bracket characters live in
<b>inside the link, so the accessible name is ”[ Download for macOS ]”; if that reads badly in your stack, wrap the brackets inaria-hidden="true"spans. - Contrast on
--paper:--ink14.8:1,--ink-27.0:1,--rust5.5:1;--rust-inkon--rust6.0:1;--paperon--ink14.8:1. Every glyph of the art meets 4.5:1 because the art uses only these three inks. - The email input keeps 3:1 against the paper through its
--ink-2border. - Tab order: logo, nav links, sign in, the two hero buttons, footer email.
Responsive rules
- ≥ 1280: as drawn; the art fills about 70 columns.
- 900–1279: hero columns keep their ratio; the art regenerates to fewer columns; the H1 shrinks with its clamp.
- < 900: hero stacks (copy, then art),
howstacks (diagram, then table), plans stack to one column, nav links hide (logo, clock and sign in remain). - < 640: gutter 16px, body 14/22, art 11/13 and 18 rows, the horizontal diagram is swapped for the 21-column vertical one, the nav “sign in” hides, plans gap 28px so the tab titles clear the card above. At 390 nothing scrolls sideways.
Acceptance checklist
Always
- Every glyph in the art, frames, diagram and buttons is in U+0020–U+007E (copy may add
·from Latin-1); no box-drawing or block characters anywhere. - One monospace family;
font-variant-ligatures: noneon body. - The art is generated to
floor(column width / measured ch width)columns and redraws on resize. - The art uses at most three inks, each ≥ 4.5:1 on the paper.
- Bracket buttons fill left to right over 220ms on hover and keyboard focus, and brackets change colour with the label.
- The typing line types at 70ms per char and shows the full text with no cycling under reduced motion.
- Below 640px the diagram swaps to the vertical version and the page has no horizontal scroll at 390.
- All interactive elements show a 2px dashed rust focus ring at 3px offset.
- The art and diagram have
role="img"and a written description.
This demo
- H1 reads “Notes that stay .txt” with “.txt” in
#A8391B. - The three typing commands and results match Reference behaviour 3.
- The packet crosses the wire in 4.8s; caption shows
relay eu-2 · packet NN%. - Plans are solo $0, plus $4 / month (inverted), self-host $29 once.
- Nav clock shows
HH:MM:SS localand ticks every second.
Implementation notes
- Measure the character cell in the face that is actually showing, and measure again on
document.fonts.readyand on resize. The web font can arrive after the first draw, and some platforms (Linux) draw mono glyphs wider, so a column count taken once at start can run off a 390px screen. Keep one column of slack.
Measure the character cell, then generate to fit. Never hard-code columns; fonts differ by a fraction of a pixel and the right edge ragged-breaks.
const probe = document.createElement('span');
probe.textContent = '0000000000'; probe.style.visibility = 'hidden';
art.appendChild(probe);
const cw = probe.getBoundingClientRect().width / 10 || 7.8; probe.remove();
const cols = Math.max(30, Math.floor(art.clientWidth / cw));
const lh = parseFloat(getComputedStyle(art).lineHeight);
const asp = cw / lh; // scale x by this so a circle stays round
Paint order for the art. For each cell: sky (hash-seeded . and *, never Math.random, so it is stable between frames), then the sun (a circle in row units, ramp @%#*+=-:. by row, blank every other row below its centre for sunset bands), then three ridges far to near (edge glyph from the slope: / rising, \ falling, _ flat; fill ., :, /), then poles | with -+- caps, then the wire. The wire uses - when the curve’s fractional row is < 0.5 and _ otherwise, which doubles its vertical resolution.
const wire = x => { const t = (x - p1) / (p2 - p1); return top + .4 + rows * .13 * (1 - (2 * t - 1) ** 2); };
if (x > p1 && x < p2) { const w = wire(x), wy = Math.floor(w);
if (y === wy) { ch = (w - wy) < .5 ? '-' : '_'; if (x === pk) { ch = 'o'; c = 'p'; } } }
Brackets that go solid without shifting layout: the fill is a pseudo-element behind the text, scaled from 0.
.btn { position: relative; isolation: isolate; height: 40px; }
.btn::after { content: ""; position: absolute; inset: 6px 0; background: var(--ink);
transform: scaleX(0); transform-origin: left; transition: transform 220ms var(--ease); z-index: -1; }
.btn:hover::after, .btn:focus-visible::after { transform: scaleX(1); }
.btn:hover, .btn:hover b { color: var(--paper); }
Common mistakes: using │ ┌ █ from a cheat sheet (they fall back to Menlo or DejaVu with a different advance and the frame tears); letting the art <pre> set the column width (give its grid track minmax(0,1fr) and the pre overflow: hidden); greying the far ridges below 4.5:1 for “atmosphere”, which fails contrast because every glyph is text.
Details
- Palette
- Type
- IBM Plex Mono
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
LinearShow the product doing the work. Fade it at the edges and keep one colour for what matters.
All sources