Light JSMIT licensed

ASCII Art Terminal Interface

A character-cell console on paper white rather than the usual black: box-drawn panels, a block-letter banner, and a working command line that takes input without a form element. The banner is aria-hidden with a real text alternative, because ASCII art read aloud is several hundred punctuation marks and no information.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time

Untitled UI · Affiliate link

Get Untitled UI Pro

The code

<section class="rt-16" aria-labelledby="rt-16-title">
  <h2 class="rt-16__sr" id="rt-16-title">ASCII art terminal console</h2>

  <div class="rt-16__theme">
    <input class="rt-16__theme-input" type="checkbox" id="rt-16-theme">
    <label class="rt-16__theme-label" for="rt-16-theme">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 4a8 8 0 0 0 0 16Z" fill="currentColor"/></svg>
      <span class="rt-16__sr">Toggle dark console</span>
    </label>
  </div>

  <div class="rt-16__stage">
    <p class="rt-16__badge">Character-cell console &middot; code page 437 frames &middot; ink on paper</p>

    <div class="rt-16__term">
      <div class="rt-16__banner-wrap">
        <pre class="rt-16__banner" aria-hidden="true">  ████      ██████    ██████  ██████  ██████
██    ██  ██        ██          ██      ██  
████████    ████    ██          ██      ██  
██    ██        ██  ██          ██      ██  
██    ██  ██████      ██████  ██████  ██████</pre>
        <p class="rt-16__sr">Block-letter banner reading ASCII, drawn in full-block characters.</p>
      </div>

      <div class="rt-16__frames">
        <pre class="rt-16__frame" aria-hidden="true">┌─ SESSION ──────────────────┐
│ host   northgate.local     │
│ tty    /dev/ttyS0          │
│ rate   9600 8N1            │
│ codec  cp437               │
└────────────────────────────┘</pre>
        <p class="rt-16__sr">Session panel: host northgate.local, tty /dev/ttyS0, rate 9600 8N1, codec cp437.</p>

        <pre class="rt-16__frame" aria-hidden="true">╔═ COMMANDS ═════════════════╗
║ help    list commands      ║
║ ver     print version      ║
║ dir     list files         ║
║ clear   empty the log      ║
╚════════════════════════════╝</pre>
        <p class="rt-16__sr">Commands panel: help lists commands, ver prints the version, dir lists files, clear empties the log.</p>
      </div>

      <ul class="rt-16__log" data-rt-16-log aria-live="polite" aria-relevant="additions" aria-label="Console output">
        <li class="rt-16__line">northgate console ready. type <b>help</b> and press Enter.</li>
      </ul>

      <div class="rt-16__prompt">
        <label class="rt-16__ps1" for="rt-16-input">C:\&gt;</label>
        <input class="rt-16__input" id="rt-16-input" type="text" data-rt-16-input autocomplete="off" spellcheck="false" placeholder="help">
        <button class="rt-16__run" type="button" data-rt-16-run>Run</button>
      </div>
    </div>
  </div>
</section>
.rt-16 {
  --rt-16-cell: clamp(0.58rem, 0.42rem + 0.85vw, 0.85rem);
  --rt-16-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
  --rt-16-paper: #f6f4ec;
  --rt-16-paper: oklch(97% 0.008 96);
  --rt-16-ink: #1a1a16;
  --rt-16-ink: oklch(21% 0.008 96);
  --rt-16-ink-2: #5a584e;
  --rt-16-ink-2: oklch(47% 0.012 96);
  --rt-16-line: #b6b3a5;
  --rt-16-line: oklch(76% 0.014 96);
  --rt-16-bg: #e2dfd4;
  --rt-16-bg: oklch(90% 0.012 96);
  --rt-16-acc: #8a3a08;
  --rt-16-acc: oklch(43% 0.13 55);
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--rt-16-bg);
  color: var(--rt-16-ink);
  font-family: var(--rt-16-mono);
  -webkit-text-size-adjust: 100%;
}

.rt-16 *,
.rt-16 *::before,
.rt-16 *::after {
  box-sizing: border-box;
}

@media (prefers-color-scheme: dark) {
  .rt-16 {
    --rt-16-paper: #16160f;
    --rt-16-paper: oklch(20% 0.01 96);
    --rt-16-ink: #f0eee2;
    --rt-16-ink: oklch(95% 0.008 96);
    --rt-16-ink-2: #aaa799;
    --rt-16-ink-2: oklch(73% 0.013 96);
    --rt-16-line: #464438;
    --rt-16-line: oklch(38% 0.014 96);
    --rt-16-bg: #0e0e09;
    --rt-16-bg: oklch(16% 0.008 96);
    --rt-16-acc: #ff9a52;
    --rt-16-acc: oklch(78% 0.13 55);
  }
}

.rt-16[data-theme="dark"],
.rt-16:has(.rt-16__theme-input:checked) {
  --rt-16-paper: #16160f;
  --rt-16-paper: oklch(20% 0.01 96);
  --rt-16-ink: #f0eee2;
  --rt-16-ink: oklch(95% 0.008 96);
  --rt-16-ink-2: #aaa799;
  --rt-16-ink-2: oklch(73% 0.013 96);
  --rt-16-line: #464438;
  --rt-16-line: oklch(38% 0.014 96);
  --rt-16-bg: #0e0e09;
  --rt-16-bg: oklch(16% 0.008 96);
  --rt-16-acc: #ff9a52;
  --rt-16-acc: oklch(78% 0.13 55);
}

@media (prefers-color-scheme: light) {
  .rt-16:has(.rt-16__theme-input:checked) {
    --rt-16-paper: #16160f;
    --rt-16-paper: oklch(20% 0.01 96);
    --rt-16-ink: #f0eee2;
    --rt-16-ink: oklch(95% 0.008 96);
    --rt-16-ink-2: #aaa799;
    --rt-16-ink-2: oklch(73% 0.013 96);
    --rt-16-line: #464438;
    --rt-16-line: oklch(38% 0.014 96);
    --rt-16-bg: #0e0e09;
    --rt-16-bg: oklch(16% 0.008 96);
    --rt-16-acc: #ff9a52;
    --rt-16-acc: oklch(78% 0.13 55);
  }
}

@media (prefers-color-scheme: dark) {
  .rt-16:has(.rt-16__theme-input:checked) {
    --rt-16-paper: #f6f4ec;
    --rt-16-paper: oklch(97% 0.008 96);
    --rt-16-ink: #1a1a16;
    --rt-16-ink: oklch(21% 0.008 96);
    --rt-16-ink-2: #5a584e;
    --rt-16-ink-2: oklch(47% 0.012 96);
    --rt-16-line: #b6b3a5;
    --rt-16-line: oklch(76% 0.014 96);
    --rt-16-bg: #e2dfd4;
    --rt-16-bg: oklch(90% 0.012 96);
    --rt-16-acc: #8a3a08;
    --rt-16-acc: oklch(43% 0.13 55);
  }
}

.rt-16__sr {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.rt-16__theme {
  position: absolute;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
  z-index: 6;
}

.rt-16__theme-input {
  position: absolute;
  inset: 0;
  inline-size: 44px;
  block-size: 44px;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.rt-16__theme-label {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid var(--rt-16-line);
  background: var(--rt-16-paper);
  color: var(--rt-16-ink-2);
  cursor: pointer;
}

.rt-16__theme-label svg {
  inline-size: 20px;
  block-size: 20px;
}

.rt-16__theme-input:focus-visible + .rt-16__theme-label {
  outline: 2px solid var(--rt-16-acc);
  outline-offset: 2px;
}

.rt-16__stage {
  max-inline-size: 62rem;
  margin-inline: auto;
  padding: clamp(3.5rem, 3rem + 3vw, 5rem) clamp(0.6rem, 0.4rem + 1.5vw, 2rem) 4rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
}

.rt-16__badge {
  margin: 0;
  justify-self: start;
  min-inline-size: 0;
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rt-16-ink-2);
  border: 1px solid var(--rt-16-line);
  padding: 0.35rem 0.7rem;
  background: var(--rt-16-paper);
}

.rt-16__term {
  min-inline-size: 0;
  background: var(--rt-16-paper);
  border: 2px solid var(--rt-16-ink);
  padding: clamp(0.75rem, 0.5rem + 1.4vw, 1.4rem);
  display: grid;
  gap: 1rem;
  box-shadow: 6px 6px 0 0 var(--rt-16-line);
}

.rt-16__banner-wrap {
  min-inline-size: 0;
  overflow-x: auto;
}

.rt-16__banner {
  margin: 0;
  font-family: inherit;
  white-space: pre;
  line-height: 1;
  font-size: calc(var(--rt-16-cell) * 0.9);
  color: var(--rt-16-acc);
  min-inline-size: 0;
}

.rt-16__frames {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.85rem;
}

.rt-16__frame {
  margin: 0;
  font-family: inherit;
  white-space: pre;
  line-height: 1.2;
  overflow-x: auto;
  font-size: var(--rt-16-cell);
  color: var(--rt-16-ink);
  min-inline-size: 0;
}

.rt-16__log {
  list-style: none;
  margin: 0;
  padding: 0.6rem 0.7rem;
  min-inline-size: 0;
  border: 1px solid var(--rt-16-line);
  background: var(--rt-16-bg);
  display: grid;
  gap: 0.2rem;
  align-content: start;
  min-block-size: 9rem;
  max-block-size: 16rem;
  overflow-y: auto;
  font-size: var(--rt-16-cell);
}

.rt-16__line {
  min-inline-size: 0;
  overflow-wrap: anywhere;
  color: var(--rt-16-ink);
}

.rt-16__line b {
  color: var(--rt-16-acc);
}

.rt-16__line--echo {
  color: var(--rt-16-ink-2);
}

.rt-16__prompt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  min-inline-size: 0;
}

.rt-16__ps1 {
  font-size: var(--rt-16-cell);
  color: var(--rt-16-acc);
  flex: none;
  letter-spacing: 0.05em;
}

.rt-16__input {
  flex: 1 1 10rem;
  min-inline-size: 0;
  min-block-size: 44px;
  padding: 0.5rem 0.7rem;
  font: inherit;
  font-size: var(--rt-16-cell);
  color: var(--rt-16-ink);
  background: var(--rt-16-bg);
  border: 1px solid var(--rt-16-ink);
}

.rt-16__input:focus-visible {
  outline: 2px solid var(--rt-16-acc);
  outline-offset: 2px;
}

.rt-16__run {
  min-block-size: 44px;
  min-inline-size: 5rem;
  padding: 0.5rem 1rem;
  cursor: pointer;
  font: inherit;
  font-size: var(--rt-16-cell);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  background: var(--rt-16-ink);
  color: var(--rt-16-paper);
  border: 1px solid var(--rt-16-ink);
}

.rt-16__run:active {
  background: var(--rt-16-acc);
  border-color: var(--rt-16-acc);
}

.rt-16__run:focus-visible {
  outline: 2px solid var(--rt-16-acc);
  outline-offset: 2px;
}

@media (min-width: 46rem) {
  .rt-16__frames {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
const root = document.currentScript.closest('.rt-16') || document.querySelector('.rt-16');
const log = root.querySelector('[data-rt-16-log]');
const input = root.querySelector('[data-rt-16-input]');
const run = root.querySelector('[data-rt-16-run]');
const replies = {
  help: 'commands: help, ver, dir, clear',
  ver: 'northgate console 2.4 — cp437, 9600 8N1',
  dir: 'AUTOEXEC.BAT  COMMAND.COM  CONFIG.SYS  README.TXT'
};
const say = (text, echo) => {
  const li = document.createElement('li');
  li.className = echo ? 'rt-16__line rt-16__line--echo' : 'rt-16__line';
  li.textContent = text;
  log.append(li);
  while (log.childElementCount > 40) log.firstElementChild.remove();
};
const submit = () => {
  const cmd = input.value.trim().toLowerCase();
  if (!cmd) return;
  input.value = '';
  if (cmd === 'clear') { log.replaceChildren(); return; }
  say('C:\\> ' + cmd, true);
  say(replies[cmd] || ('bad command or file name: ' + cmd));
};
const onKey = (e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } };
run.addEventListener('click', submit);
input.addEventListener('keydown', onKey);
root.__rtCleanup = () => { run.removeEventListener('click', submit); input.removeEventListener('keydown', onKey); };
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Retro UI from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: ASCII Art Terminal Interface
Source: https://codefronts.com/design-styles/css-retro-designs/ascii-art-web-interface-code/

A character-cell console on paper white rather than the usual black: box-drawn panels, a block-letter banner, and a working command line that takes input without a form element. The banner is aria-hidden with a real text alternative, because ASCII art read aloud is several hundred punctuation marks and no information.
## HTML
```html
<section class="rt-16" aria-labelledby="rt-16-title">
  <h2 class="rt-16__sr" id="rt-16-title">ASCII art terminal console</h2>

  <div class="rt-16__theme">
    <input class="rt-16__theme-input" type="checkbox" id="rt-16-theme">
    <label class="rt-16__theme-label" for="rt-16-theme">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 4a8 8 0 0 0 0 16Z" fill="currentColor"/></svg>
      <span class="rt-16__sr">Toggle dark console</span>
    </label>
  </div>

  <div class="rt-16__stage">
    <p class="rt-16__badge">Character-cell console &middot; code page 437 frames &middot; ink on paper</p>

    <div class="rt-16__term">
      <div class="rt-16__banner-wrap">
        <pre class="rt-16__banner" aria-hidden="true">  ████      ██████    ██████  ██████  ██████
██    ██  ██        ██          ██      ██  
████████    ████    ██          ██      ██  
██    ██        ██  ██          ██      ██  
██    ██  ██████      ██████  ██████  ██████</pre>
        <p class="rt-16__sr">Block-letter banner reading ASCII, drawn in full-block characters.</p>
      </div>

      <div class="rt-16__frames">
        <pre class="rt-16__frame" aria-hidden="true">┌─ SESSION ──────────────────┐
│ host   northgate.local     │
│ tty    /dev/ttyS0          │
│ rate   9600 8N1            │
│ codec  cp437               │
└────────────────────────────┘</pre>
        <p class="rt-16__sr">Session panel: host northgate.local, tty /dev/ttyS0, rate 9600 8N1, codec cp437.</p>

        <pre class="rt-16__frame" aria-hidden="true">╔═ COMMANDS ═════════════════╗
║ help    list commands      ║
║ ver     print version      ║
║ dir     list files         ║
║ clear   empty the log      ║
╚════════════════════════════╝</pre>
        <p class="rt-16__sr">Commands panel: help lists commands, ver prints the version, dir lists files, clear empties the log.</p>
      </div>

      <ul class="rt-16__log" data-rt-16-log aria-live="polite" aria-relevant="additions" aria-label="Console output">
        <li class="rt-16__line">northgate console ready. type <b>help</b> and press Enter.</li>
      </ul>

      <div class="rt-16__prompt">
        <label class="rt-16__ps1" for="rt-16-input">C:\&gt;</label>
        <input class="rt-16__input" id="rt-16-input" type="text" data-rt-16-input autocomplete="off" spellcheck="false" placeholder="help">
        <button class="rt-16__run" type="button" data-rt-16-run>Run</button>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
.rt-16 {
  --rt-16-cell: clamp(0.58rem, 0.42rem + 0.85vw, 0.85rem);
  --rt-16-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
  --rt-16-paper: #f6f4ec;
  --rt-16-paper: oklch(97% 0.008 96);
  --rt-16-ink: #1a1a16;
  --rt-16-ink: oklch(21% 0.008 96);
  --rt-16-ink-2: #5a584e;
  --rt-16-ink-2: oklch(47% 0.012 96);
  --rt-16-line: #b6b3a5;
  --rt-16-line: oklch(76% 0.014 96);
  --rt-16-bg: #e2dfd4;
  --rt-16-bg: oklch(90% 0.012 96);
  --rt-16-acc: #8a3a08;
  --rt-16-acc: oklch(43% 0.13 55);
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--rt-16-bg);
  color: var(--rt-16-ink);
  font-family: var(--rt-16-mono);
  -webkit-text-size-adjust: 100%;
}

.rt-16 *,
.rt-16 *::before,
.rt-16 *::after {
  box-sizing: border-box;
}

@media (prefers-color-scheme: dark) {
  .rt-16 {
    --rt-16-paper: #16160f;
    --rt-16-paper: oklch(20% 0.01 96);
    --rt-16-ink: #f0eee2;
    --rt-16-ink: oklch(95% 0.008 96);
    --rt-16-ink-2: #aaa799;
    --rt-16-ink-2: oklch(73% 0.013 96);
    --rt-16-line: #464438;
    --rt-16-line: oklch(38% 0.014 96);
    --rt-16-bg: #0e0e09;
    --rt-16-bg: oklch(16% 0.008 96);
    --rt-16-acc: #ff9a52;
    --rt-16-acc: oklch(78% 0.13 55);
  }
}

.rt-16[data-theme="dark"],
.rt-16:has(.rt-16__theme-input:checked) {
  --rt-16-paper: #16160f;
  --rt-16-paper: oklch(20% 0.01 96);
  --rt-16-ink: #f0eee2;
  --rt-16-ink: oklch(95% 0.008 96);
  --rt-16-ink-2: #aaa799;
  --rt-16-ink-2: oklch(73% 0.013 96);
  --rt-16-line: #464438;
  --rt-16-line: oklch(38% 0.014 96);
  --rt-16-bg: #0e0e09;
  --rt-16-bg: oklch(16% 0.008 96);
  --rt-16-acc: #ff9a52;
  --rt-16-acc: oklch(78% 0.13 55);
}

@media (prefers-color-scheme: light) {
  .rt-16:has(.rt-16__theme-input:checked) {
    --rt-16-paper: #16160f;
    --rt-16-paper: oklch(20% 0.01 96);
    --rt-16-ink: #f0eee2;
    --rt-16-ink: oklch(95% 0.008 96);
    --rt-16-ink-2: #aaa799;
    --rt-16-ink-2: oklch(73% 0.013 96);
    --rt-16-line: #464438;
    --rt-16-line: oklch(38% 0.014 96);
    --rt-16-bg: #0e0e09;
    --rt-16-bg: oklch(16% 0.008 96);
    --rt-16-acc: #ff9a52;
    --rt-16-acc: oklch(78% 0.13 55);
  }
}

@media (prefers-color-scheme: dark) {
  .rt-16:has(.rt-16__theme-input:checked) {
    --rt-16-paper: #f6f4ec;
    --rt-16-paper: oklch(97% 0.008 96);
    --rt-16-ink: #1a1a16;
    --rt-16-ink: oklch(21% 0.008 96);
    --rt-16-ink-2: #5a584e;
    --rt-16-ink-2: oklch(47% 0.012 96);
    --rt-16-line: #b6b3a5;
    --rt-16-line: oklch(76% 0.014 96);
    --rt-16-bg: #e2dfd4;
    --rt-16-bg: oklch(90% 0.012 96);
    --rt-16-acc: #8a3a08;
    --rt-16-acc: oklch(43% 0.13 55);
  }
}

.rt-16__sr {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.rt-16__theme {
  position: absolute;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
  z-index: 6;
}

.rt-16__theme-input {
  position: absolute;
  inset: 0;
  inline-size: 44px;
  block-size: 44px;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.rt-16__theme-label {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid var(--rt-16-line);
  background: var(--rt-16-paper);
  color: var(--rt-16-ink-2);
  cursor: pointer;
}

.rt-16__theme-label svg {
  inline-size: 20px;
  block-size: 20px;
}

.rt-16__theme-input:focus-visible + .rt-16__theme-label {
  outline: 2px solid var(--rt-16-acc);
  outline-offset: 2px;
}

.rt-16__stage {
  max-inline-size: 62rem;
  margin-inline: auto;
  padding: clamp(3.5rem, 3rem + 3vw, 5rem) clamp(0.6rem, 0.4rem + 1.5vw, 2rem) 4rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
}

.rt-16__badge {
  margin: 0;
  justify-self: start;
  min-inline-size: 0;
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rt-16-ink-2);
  border: 1px solid var(--rt-16-line);
  padding: 0.35rem 0.7rem;
  background: var(--rt-16-paper);
}

.rt-16__term {
  min-inline-size: 0;
  background: var(--rt-16-paper);
  border: 2px solid var(--rt-16-ink);
  padding: clamp(0.75rem, 0.5rem + 1.4vw, 1.4rem);
  display: grid;
  gap: 1rem;
  box-shadow: 6px 6px 0 0 var(--rt-16-line);
}

.rt-16__banner-wrap {
  min-inline-size: 0;
  overflow-x: auto;
}

.rt-16__banner {
  margin: 0;
  font-family: inherit;
  white-space: pre;
  line-height: 1;
  font-size: calc(var(--rt-16-cell) * 0.9);
  color: var(--rt-16-acc);
  min-inline-size: 0;
}

.rt-16__frames {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.85rem;
}

.rt-16__frame {
  margin: 0;
  font-family: inherit;
  white-space: pre;
  line-height: 1.2;
  overflow-x: auto;
  font-size: var(--rt-16-cell);
  color: var(--rt-16-ink);
  min-inline-size: 0;
}

.rt-16__log {
  list-style: none;
  margin: 0;
  padding: 0.6rem 0.7rem;
  min-inline-size: 0;
  border: 1px solid var(--rt-16-line);
  background: var(--rt-16-bg);
  display: grid;
  gap: 0.2rem;
  align-content: start;
  min-block-size: 9rem;
  max-block-size: 16rem;
  overflow-y: auto;
  font-size: var(--rt-16-cell);
}

.rt-16__line {
  min-inline-size: 0;
  overflow-wrap: anywhere;
  color: var(--rt-16-ink);
}

.rt-16__line b {
  color: var(--rt-16-acc);
}

.rt-16__line--echo {
  color: var(--rt-16-ink-2);
}

.rt-16__prompt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  min-inline-size: 0;
}

.rt-16__ps1 {
  font-size: var(--rt-16-cell);
  color: var(--rt-16-acc);
  flex: none;
  letter-spacing: 0.05em;
}

.rt-16__input {
  flex: 1 1 10rem;
  min-inline-size: 0;
  min-block-size: 44px;
  padding: 0.5rem 0.7rem;
  font: inherit;
  font-size: var(--rt-16-cell);
  color: var(--rt-16-ink);
  background: var(--rt-16-bg);
  border: 1px solid var(--rt-16-ink);
}

.rt-16__input:focus-visible {
  outline: 2px solid var(--rt-16-acc);
  outline-offset: 2px;
}

.rt-16__run {
  min-block-size: 44px;
  min-inline-size: 5rem;
  padding: 0.5rem 1rem;
  cursor: pointer;
  font: inherit;
  font-size: var(--rt-16-cell);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  background: var(--rt-16-ink);
  color: var(--rt-16-paper);
  border: 1px solid var(--rt-16-ink);
}

.rt-16__run:active {
  background: var(--rt-16-acc);
  border-color: var(--rt-16-acc);
}

.rt-16__run:focus-visible {
  outline: 2px solid var(--rt-16-acc);
  outline-offset: 2px;
}

@media (min-width: 46rem) {
  .rt-16__frames {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
```

## JavaScript
```js
const root = document.currentScript.closest('.rt-16') || document.querySelector('.rt-16');
const log = root.querySelector('[data-rt-16-log]');
const input = root.querySelector('[data-rt-16-input]');
const run = root.querySelector('[data-rt-16-run]');
const replies = {
  help: 'commands: help, ver, dir, clear',
  ver: 'northgate console 2.4 — cp437, 9600 8N1',
  dir: 'AUTOEXEC.BAT  COMMAND.COM  CONFIG.SYS  README.TXT'
};
const say = (text, echo) => {
  const li = document.createElement('li');
  li.className = echo ? 'rt-16__line rt-16__line--echo' : 'rt-16__line';
  li.textContent = text;
  log.append(li);
  while (log.childElementCount > 40) log.firstElementChild.remove();
};
const submit = () => {
  const cmd = input.value.trim().toLowerCase();
  if (!cmd) return;
  input.value = '';
  if (cmd === 'clear') { log.replaceChildren(); return; }
  say('C:\\> ' + cmd, true);
  say(replies[cmd] || ('bad command or file name: ' + cmd));
};
const onKey = (e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } };
run.addEventListener('click', submit);
input.addEventListener('keydown', onKey);
root.__rtCleanup = () => { run.removeEventListener('click', submit); input.removeEventListener('keydown', onKey); };
```

How this works

ASCII art is decoration and must be marked as such. A screen reader handed a block-letter banner reads out several hundred instances of a full block character, or worse, spells the punctuation. The fix is two lines: aria-hidden="true" on the art, and a visually hidden element next to it that says what the art depicts. That is a complete solution and it takes less effort than the banner did.

The console cannot use a form. Demo pages here run inside an iframe with scripts allowed and forms not allowed, so a submit event never fires and a form-based command line silently does nothing. The working pattern is a plain input, a <button type="button"> with a click handler, and a keydown listener that treats Enter as the same action. It is also the more robust pattern generally, because it does not depend on implicit submission behaviour that differs between browsers when there is one field.

A light-ground terminal is a real thing, not a modern reinterpretation. Printing terminals put dark ink on paper, and plenty of later console software shipped a light scheme. Choosing it here does two jobs at once: it is historically defensible, and it keeps this demo visually distinct from the several dark-screen demos elsewhere in the collection. The box-drawing characters carry the era; the ground does not have to.

The live region has to be narrow. Marking the entire output log as a live region means every command replays the whole history. The region is the log element with aria-relevant="additions", so only the appended line is announced, and the console banner and prompt are outside it entirely. Get this wrong and the interface is technically accessible and practically unusable.

Make it yours

  • Change --rt-16-cell to rescale the whole character grid; the frames are characters so they scale with it.
  • Swap the ink and paper tokens for a dark console; nothing else in the layout assumes a light ground.
  • Replace the banner by generating new block-letter art and updating the hidden description to match — never leave the old description in place.
  • Extend the command set by adding entries to the responses map in the script; unknown commands already fall through to a not-found line.
  • Change the frame style from double-line to single-line by swapping the frame characters; both occupy one cell.
  • Cap the log length by lowering the trim threshold in the script if you want a shorter scrollback.

Gotchas — read before shipping

  • ASCII art without aria-hidden and a text alternative is read aloud as a wall of punctuation. It is decoration; mark it as decoration.
  • A form submit handler never fires in a sandboxed iframe without form permission. Use a button click plus an Enter keydown, which also works everywhere else.
  • Marking the whole log as a live region replays the entire history on every command. Scope the region and set it to announce additions only.
  • Box-drawing glyphs are not guaranteed to share an advance width across every fallback font. If the frame shears, the fallback stack substituted a glyph from a non-monospace face.

Browser support

ChromeSafariFirefoxEdge
111+15.4+121+111+

the ch unit, CSS grid and attribute selectors are all Baseline since 2017 and the script is plain ES2024 with no optional syntax. oklch() and the :has() theme toggle set the stated floor; below it the console renders identically and only the theme switch is inert.

Techniques used in this demo

Search CodeFronts

Loading…