Pure CSSMIT licensed

MS DOS Text Mode Interface Grid

A character-cell interface, reproduced as one: an 80×25 field where every cell is exactly one monospace advance, laid out with CSS Grid and ch units, framed with code page 437 box-drawing characters and restricted to the 16-colour CGA palette as tokens. The constraint is the subject — nothing can sit between two cells, so every alignment decision is an integer.

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-06" aria-labelledby="rt-06-title">
  <h2 class="rt-06__sr" id="rt-06-title">MS-DOS text mode character grid</h2>

  <div class="rt-06__theme">
    <input class="rt-06__theme-input" type="checkbox" id="rt-06-theme">
    <label class="rt-06__theme-label" for="rt-06-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-06__sr">Toggle dark surround</span>
    </label>
  </div>

  <div class="rt-06__stage">
    <p class="rt-06__badge">IBM CGA &middot; 16 colours &middot; 80 &times; 25 text mode &middot; code page 437</p>

    <div class="rt-06__frame">
      <div class="rt-06__screen">
        <div class="rt-06__menu" role="presentation">
          <span class="rt-06__menu-hi">F</span><span>ile</span>
          <span class="rt-06__menu-gap"></span>
          <span class="rt-06__menu-hi">E</span><span>dit</span>
          <span class="rt-06__menu-gap"></span>
          <span class="rt-06__menu-hi">S</span><span>earch</span>
          <span class="rt-06__menu-gap"></span>
          <span class="rt-06__menu-hi">O</span><span>ptions</span>
          <span class="rt-06__menu-tail">MEM 640K</span>
        </div>

        <pre class="rt-06__art rt-06__art--wide" aria-hidden="true">╔══════════════════════════════════════════════════════════════════════════════╗
║  C:\ ─ Directory of drive C  ─  6 file(s)  ─  90,671 bytes                   ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Name            Size      Date        Attr    Type                          ║
║  ────────────────────────────────────────────────────────────────────        ║
║  AUTOEXEC.BAT      1,024   04-12-1995  A       Batch file                    ║
║  COMMAND.COM      54,619   05-31-1994  RA      Command interpreter           ║
║  CONFIG.SYS          412   04-12-1995  A       System configuration          ║
║  HIMEM.SYS        29,136   05-31-1994  RA      Extended memory manager       ║
║  README.TXT        7,880   11-03-1995  A       Text                          ║
║                                                                              ║
║  Every glyph above occupies exactly one cell. Nothing sits between two cells.║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
╚══════════════════════════════════════════════════════════════════════════════╝</pre>
        <pre class="rt-06__art rt-06__art--narrow" aria-hidden="true">╔══════════════════════════════════════╗
║  C:\ ─ 6 file(s), 90,671 bytes       ║
╠══════════════════════════════════════╣
║ Name          Size    Attr           ║
║ ─────────────────────────────────────║
║ AUTOEXEC.BAT  1,024   A              ║
║ COMMAND.COM  54,619   RA             ║
║ CONFIG.SYS      412   A              ║
║ HIMEM.SYS    29,136   RA             ║
║ README.TXT    7,880   A              ║
║                                      ║
║  One glyph = one cell. No between.   ║
║                                      ║
║                                      ║
╚══════════════════════════════════════╝</pre>
        <p class="rt-06__sr">A DOS-style file manager panel listing AUTOEXEC.BAT, CONFIG.SYS, COMMAND.COM and HIMEM.SYS with sizes and dates, framed in double-line box characters.</p>

        <div class="rt-06__status">
          <span><span class="rt-06__key">F1</span> Help</span>
          <span><span class="rt-06__key">F3</span> Open</span>
          <span><span class="rt-06__key">F10</span> Menu</span>
          <span class="rt-06__status-tail">80 &times; 25</span>
        </div>
      </div>
    </div>

    <aside class="rt-06__inspect">
      <h3 class="rt-06__inspect-h">Cell inspector &mdash; the grid the text lands on</h3>
      <div class="rt-06__cells" aria-hidden="true">
        <span class="rt-06__cell">╔</span><span class="rt-06__cell">═</span><span class="rt-06__cell">═</span><span class="rt-06__cell">╦</span><span class="rt-06__cell">═</span><span class="rt-06__cell">═</span><span class="rt-06__cell">╗</span><span class="rt-06__cell rt-06__cell--hi">█</span><span class="rt-06__cell rt-06__cell--hi">▓</span><span class="rt-06__cell rt-06__cell--hi">▒</span><span class="rt-06__cell rt-06__cell--hi">░</span><span class="rt-06__cell"> </span>
        <span class="rt-06__cell">║</span><span class="rt-06__cell">A</span><span class="rt-06__cell">B</span><span class="rt-06__cell">║</span><span class="rt-06__cell">C</span><span class="rt-06__cell">D</span><span class="rt-06__cell">║</span><span class="rt-06__cell">─</span><span class="rt-06__cell">┼</span><span class="rt-06__cell">─</span><span class="rt-06__cell">┤</span><span class="rt-06__cell"> </span>
        <span class="rt-06__cell">╚</span><span class="rt-06__cell">═</span><span class="rt-06__cell">═</span><span class="rt-06__cell">╩</span><span class="rt-06__cell">═</span><span class="rt-06__cell">═</span><span class="rt-06__cell">╝</span><span class="rt-06__cell">└</span><span class="rt-06__cell">┴</span><span class="rt-06__cell">┘</span><span class="rt-06__cell"> </span><span class="rt-06__cell"> </span>
      </div>
      <p class="rt-06__inspect-p">Each box is <code>1ch</code> wide and one line-height tall. The frame is characters, not borders &mdash; a border would have to live between two cells, and there is no between.</p>
    </aside>

    <aside class="rt-06__pal">
      <h3 class="rt-06__inspect-h">CGA palette &mdash; 16 defined values</h3>
      <ul class="rt-06__pal-list">
        <li style="--rt-06-c: #000000">0 black</li>
        <li style="--rt-06-c: #0000aa">1 blue</li>
        <li style="--rt-06-c: #00aa00">2 green</li>
        <li style="--rt-06-c: #00aaaa">3 cyan</li>
        <li style="--rt-06-c: #aa0000">4 red</li>
        <li style="--rt-06-c: #aa00aa">5 magenta</li>
        <li style="--rt-06-c: #aa5500">6 brown</li>
        <li style="--rt-06-c: #aaaaaa">7 lt grey</li>
        <li style="--rt-06-c: #555555">8 dk grey</li>
        <li style="--rt-06-c: #5555ff">9 lt blue</li>
        <li style="--rt-06-c: #55ff55">10 lt green</li>
        <li style="--rt-06-c: #55ffff">11 lt cyan</li>
        <li style="--rt-06-c: #ff5555">12 lt red</li>
        <li style="--rt-06-c: #ff55ff">13 lt magenta</li>
        <li style="--rt-06-c: #ffff55">14 yellow</li>
        <li style="--rt-06-c: #ffffff">15 white</li>
      </ul>
    </aside>
  </div>
</section>
.rt-06 {
  --rt-06-cols: 80;
  --rt-06-k: #000000;
  --rt-06-blue: #0000aa;
  --rt-06-cyan: #00aaaa;
  --rt-06-ltgrey: #aaaaaa;
  --rt-06-dkgrey: #555555;
  --rt-06-ltcyan: #55ffff;
  --rt-06-yellow: #ffff55;
  --rt-06-white: #ffffff;
  --rt-06-red: #aa0000;
  --rt-06-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
  --rt-06-bg: #c9ccd2;
  --rt-06-bg: oklch(84% 0.008 260);
  --rt-06-surface: #eef0f3;
  --rt-06-surface: oklch(95% 0.004 260);
  --rt-06-ink: #14171c;
  --rt-06-ink: oklch(19% 0.008 260);
  --rt-06-ink-2: #464e58;
  --rt-06-ink-2: oklch(42% 0.014 260);
  --rt-06-line: #9aa1ab;
  --rt-06-line: oklch(71% 0.012 260);
  --rt-06-acc: #1247c9;
  --rt-06-acc: oklch(45% 0.2 264);
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--rt-06-bg);
  color: var(--rt-06-ink);
  font-family: var(--rt-06-mono);
  -webkit-text-size-adjust: 100%;
}

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

@media (prefers-color-scheme: dark) {
  .rt-06 {
    --rt-06-bg: #101318;
    --rt-06-bg: oklch(18% 0.008 260);
    --rt-06-surface: #1a1e25;
    --rt-06-surface: oklch(24% 0.01 260);
    --rt-06-ink: #eef1f5;
    --rt-06-ink: oklch(96% 0.004 260);
    --rt-06-ink-2: #a3abb6;
    --rt-06-ink-2: oklch(74% 0.012 260);
    --rt-06-line: #39414c;
    --rt-06-line: oklch(35% 0.014 260);
    --rt-06-acc: #7fa8ff;
    --rt-06-acc: oklch(76% 0.13 264);
  }
}

.rt-06[data-theme="dark"],
.rt-06:has(.rt-06__theme-input:checked) {
  --rt-06-bg: #101318;
  --rt-06-bg: oklch(18% 0.008 260);
  --rt-06-surface: #1a1e25;
  --rt-06-surface: oklch(24% 0.01 260);
  --rt-06-ink: #eef1f5;
  --rt-06-ink: oklch(96% 0.004 260);
  --rt-06-ink-2: #a3abb6;
  --rt-06-ink-2: oklch(74% 0.012 260);
  --rt-06-line: #39414c;
  --rt-06-line: oklch(35% 0.014 260);
  --rt-06-acc: #7fa8ff;
  --rt-06-acc: oklch(76% 0.13 264);
}

@media (prefers-color-scheme: light) {
  .rt-06:has(.rt-06__theme-input:checked) {
    --rt-06-bg: #101318;
    --rt-06-bg: oklch(18% 0.008 260);
    --rt-06-surface: #1a1e25;
    --rt-06-surface: oklch(24% 0.01 260);
    --rt-06-ink: #eef1f5;
    --rt-06-ink: oklch(96% 0.004 260);
    --rt-06-ink-2: #a3abb6;
    --rt-06-ink-2: oklch(74% 0.012 260);
    --rt-06-line: #39414c;
    --rt-06-line: oklch(35% 0.014 260);
    --rt-06-acc: #7fa8ff;
    --rt-06-acc: oklch(76% 0.13 264);
  }
}

@media (prefers-color-scheme: dark) {
  .rt-06:has(.rt-06__theme-input:checked) {
    --rt-06-bg: #c9ccd2;
    --rt-06-bg: oklch(84% 0.008 260);
    --rt-06-surface: #eef0f3;
    --rt-06-surface: oklch(95% 0.004 260);
    --rt-06-ink: #14171c;
    --rt-06-ink: oklch(19% 0.008 260);
    --rt-06-ink-2: #464e58;
    --rt-06-ink-2: oklch(42% 0.014 260);
    --rt-06-line: #9aa1ab;
    --rt-06-line: oklch(71% 0.012 260);
    --rt-06-acc: #1247c9;
    --rt-06-acc: oklch(45% 0.2 264);
  }
}

.rt-06__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-06__theme {
  position: absolute;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
  z-index: 6;
}

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

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

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

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

.rt-06__stage {
  max-inline-size: 72rem;
  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: 1.1rem;
}

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

.rt-06__frame {
  min-inline-size: 0;
  background: #6d6f70;
  padding: clamp(0.5rem, 0.3rem + 1vw, 1rem);
  border-radius: 6px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -2px 0 rgba(0,0,0,0.4), 0 14px 30px -16px rgba(0,0,0,0.6);
}

.rt-06__screen {
  container-type: inline-size;
  min-inline-size: 0;
  overflow: hidden;
  background: var(--rt-06-blue);
  color: var(--rt-06-ltgrey);
  padding: 0;
  border: 2px solid #1b1d1f;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
}

.rt-06__menu {
  display: flex;
  align-items: center;
  min-block-size: 1.6em;
  padding-inline: 0.5ch;
  background: var(--rt-06-ltgrey);
  color: var(--rt-06-k);
  font-size: clamp(0.66rem, 0.55rem + 0.6vw, 0.85rem);
  min-inline-size: 0;
  overflow: hidden;
  white-space: nowrap;
}
/* CGA dark red (index 4) rather than bright red (index 12): 4.71:1 on the
   light-grey menu bar where bright red is only 3.34:1, and both are real
   entries in the same 16-colour palette. */

.rt-06__menu-hi {
  color: #800000;
  font-weight: 700;
}

.rt-06__menu-gap {
  inline-size: 2ch;
  flex: none;
}

.rt-06__menu-tail {
  margin-inline-start: auto;
  color: #303030;
  padding-inline-start: 1ch;
}

.rt-06__art {
  margin: 0;
  padding: 0.5em 0;
  font-family: inherit;
  white-space: pre;
  color: var(--rt-06-ltcyan);
  min-inline-size: 0;
  overflow: hidden;
  font-size: min(0.95rem, 2.02cqi);
  line-height: 1.16;
  text-align: center;
}

@supports not (font-size: 1cqi) {
  .rt-06__art {
    font-size: clamp(0.35rem, 1.6vw, 0.95rem);
  }
}

.rt-06__art--narrow {
  display: block;
}

.rt-06__art--wide {
  display: none;
}

@media (min-width: 52rem) {
  .rt-06__art--narrow {
    display: none;
  }

  .rt-06__art--wide {
    display: block;
  }
}

.rt-06__status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 2ch;
  min-block-size: 1.6em;
  padding-inline: 0.5ch;
  background: var(--rt-06-ltgrey);
  color: var(--rt-06-k);
  font-size: clamp(0.66rem, 0.55rem + 0.6vw, 0.85rem);
  min-inline-size: 0;
}

.rt-06__key {
  color: #800000;
  font-weight: 700;
}

.rt-06__status-tail {
  margin-inline-start: auto;
  color: #303030;
}

.rt-06__inspect,
.rt-06__pal {
  min-inline-size: 0;
  background: var(--rt-06-surface);
  border: 1px solid var(--rt-06-line);
  padding: clamp(0.8rem, 0.6rem + 0.8vw, 1.1rem);
  display: grid;
  gap: 0.7rem;
  align-content: start;
}

.rt-06__inspect-h {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--rt-06-ink-2);
}

.rt-06__cells {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1px;
  background: var(--rt-06-line);
  border: 1px solid var(--rt-06-line);
  min-inline-size: 0;
  font-size: clamp(0.8rem, 0.6rem + 1.4vw, 1.35rem);
}

.rt-06__cell {
  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  min-inline-size: 0;
  background: var(--rt-06-blue);
  color: var(--rt-06-ltcyan);
  line-height: 1;
}

.rt-06__cell--hi {
  color: var(--rt-06-yellow);
}

.rt-06__inspect-p {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.55;
  color: var(--rt-06-ink-2);
  min-inline-size: 0;
  text-wrap: pretty;
}

.rt-06__inspect-p code {
  background: var(--rt-06-bg);
  padding: 0.05em 0.3em;
}

.rt-06__pal-list {
  list-style: none;
  margin: 0;
  padding: 0;
  min-inline-size: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 7.5rem), 1fr));
  gap: 2px;
}

.rt-06__pal-list li {
  min-inline-size: 0;
  font-size: 0.68rem;
  padding: 0.4rem 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--rt-06-ink-2);
  border: 1px solid var(--rt-06-line);
}

.rt-06__pal-list li::before {
  content: "";
  inline-size: 16px;
  block-size: 16px;
  flex: none;
  background: var(--rt-06-c);
  border: 1px solid var(--rt-06-line);
}

@media (min-width: 56rem) {
  .rt-06__stage {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .rt-06__badge,
    .rt-06__frame {
    grid-column: 1 / -1;
  }
}
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: MS DOS Text Mode Interface Grid
Source: https://codefronts.com/design-styles/css-retro-designs/ms-dos-text-mode-interface-grid/

A character-cell interface, reproduced as one: an 80&times;25 field where every cell is exactly one monospace advance, laid out with CSS Grid and ch units, framed with code page 437 box-drawing characters and restricted to the 16-colour CGA palette as tokens. The constraint is the subject &mdash; nothing can sit between two cells, so every alignment decision is an integer.
## HTML
```html
<section class="rt-06" aria-labelledby="rt-06-title">
  <h2 class="rt-06__sr" id="rt-06-title">MS-DOS text mode character grid</h2>

  <div class="rt-06__theme">
    <input class="rt-06__theme-input" type="checkbox" id="rt-06-theme">
    <label class="rt-06__theme-label" for="rt-06-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-06__sr">Toggle dark surround</span>
    </label>
  </div>

  <div class="rt-06__stage">
    <p class="rt-06__badge">IBM CGA &middot; 16 colours &middot; 80 &times; 25 text mode &middot; code page 437</p>

    <div class="rt-06__frame">
      <div class="rt-06__screen">
        <div class="rt-06__menu" role="presentation">
          <span class="rt-06__menu-hi">F</span><span>ile</span>
          <span class="rt-06__menu-gap"></span>
          <span class="rt-06__menu-hi">E</span><span>dit</span>
          <span class="rt-06__menu-gap"></span>
          <span class="rt-06__menu-hi">S</span><span>earch</span>
          <span class="rt-06__menu-gap"></span>
          <span class="rt-06__menu-hi">O</span><span>ptions</span>
          <span class="rt-06__menu-tail">MEM 640K</span>
        </div>

        <pre class="rt-06__art rt-06__art--wide" aria-hidden="true">╔══════════════════════════════════════════════════════════════════════════════╗
║  C:\ ─ Directory of drive C  ─  6 file(s)  ─  90,671 bytes                   ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Name            Size      Date        Attr    Type                          ║
║  ────────────────────────────────────────────────────────────────────        ║
║  AUTOEXEC.BAT      1,024   04-12-1995  A       Batch file                    ║
║  COMMAND.COM      54,619   05-31-1994  RA      Command interpreter           ║
║  CONFIG.SYS          412   04-12-1995  A       System configuration          ║
║  HIMEM.SYS        29,136   05-31-1994  RA      Extended memory manager       ║
║  README.TXT        7,880   11-03-1995  A       Text                          ║
║                                                                              ║
║  Every glyph above occupies exactly one cell. Nothing sits between two cells.║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
║                                                                              ║
╚══════════════════════════════════════════════════════════════════════════════╝</pre>
        <pre class="rt-06__art rt-06__art--narrow" aria-hidden="true">╔══════════════════════════════════════╗
║  C:\ ─ 6 file(s), 90,671 bytes       ║
╠══════════════════════════════════════╣
║ Name          Size    Attr           ║
║ ─────────────────────────────────────║
║ AUTOEXEC.BAT  1,024   A              ║
║ COMMAND.COM  54,619   RA             ║
║ CONFIG.SYS      412   A              ║
║ HIMEM.SYS    29,136   RA             ║
║ README.TXT    7,880   A              ║
║                                      ║
║  One glyph = one cell. No between.   ║
║                                      ║
║                                      ║
╚══════════════════════════════════════╝</pre>
        <p class="rt-06__sr">A DOS-style file manager panel listing AUTOEXEC.BAT, CONFIG.SYS, COMMAND.COM and HIMEM.SYS with sizes and dates, framed in double-line box characters.</p>

        <div class="rt-06__status">
          <span><span class="rt-06__key">F1</span> Help</span>
          <span><span class="rt-06__key">F3</span> Open</span>
          <span><span class="rt-06__key">F10</span> Menu</span>
          <span class="rt-06__status-tail">80 &times; 25</span>
        </div>
      </div>
    </div>

    <aside class="rt-06__inspect">
      <h3 class="rt-06__inspect-h">Cell inspector &mdash; the grid the text lands on</h3>
      <div class="rt-06__cells" aria-hidden="true">
        <span class="rt-06__cell">╔</span><span class="rt-06__cell">═</span><span class="rt-06__cell">═</span><span class="rt-06__cell">╦</span><span class="rt-06__cell">═</span><span class="rt-06__cell">═</span><span class="rt-06__cell">╗</span><span class="rt-06__cell rt-06__cell--hi">█</span><span class="rt-06__cell rt-06__cell--hi">▓</span><span class="rt-06__cell rt-06__cell--hi">▒</span><span class="rt-06__cell rt-06__cell--hi">░</span><span class="rt-06__cell"> </span>
        <span class="rt-06__cell">║</span><span class="rt-06__cell">A</span><span class="rt-06__cell">B</span><span class="rt-06__cell">║</span><span class="rt-06__cell">C</span><span class="rt-06__cell">D</span><span class="rt-06__cell">║</span><span class="rt-06__cell">─</span><span class="rt-06__cell">┼</span><span class="rt-06__cell">─</span><span class="rt-06__cell">┤</span><span class="rt-06__cell"> </span>
        <span class="rt-06__cell">╚</span><span class="rt-06__cell">═</span><span class="rt-06__cell">═</span><span class="rt-06__cell">╩</span><span class="rt-06__cell">═</span><span class="rt-06__cell">═</span><span class="rt-06__cell">╝</span><span class="rt-06__cell">└</span><span class="rt-06__cell">┴</span><span class="rt-06__cell">┘</span><span class="rt-06__cell"> </span><span class="rt-06__cell"> </span>
      </div>
      <p class="rt-06__inspect-p">Each box is <code>1ch</code> wide and one line-height tall. The frame is characters, not borders &mdash; a border would have to live between two cells, and there is no between.</p>
    </aside>

    <aside class="rt-06__pal">
      <h3 class="rt-06__inspect-h">CGA palette &mdash; 16 defined values</h3>
      <ul class="rt-06__pal-list">
        <li style="--rt-06-c: #000000">0 black</li>
        <li style="--rt-06-c: #0000aa">1 blue</li>
        <li style="--rt-06-c: #00aa00">2 green</li>
        <li style="--rt-06-c: #00aaaa">3 cyan</li>
        <li style="--rt-06-c: #aa0000">4 red</li>
        <li style="--rt-06-c: #aa00aa">5 magenta</li>
        <li style="--rt-06-c: #aa5500">6 brown</li>
        <li style="--rt-06-c: #aaaaaa">7 lt grey</li>
        <li style="--rt-06-c: #555555">8 dk grey</li>
        <li style="--rt-06-c: #5555ff">9 lt blue</li>
        <li style="--rt-06-c: #55ff55">10 lt green</li>
        <li style="--rt-06-c: #55ffff">11 lt cyan</li>
        <li style="--rt-06-c: #ff5555">12 lt red</li>
        <li style="--rt-06-c: #ff55ff">13 lt magenta</li>
        <li style="--rt-06-c: #ffff55">14 yellow</li>
        <li style="--rt-06-c: #ffffff">15 white</li>
      </ul>
    </aside>
  </div>
</section>
```
## CSS
```css
.rt-06 {
  --rt-06-cols: 80;
  --rt-06-k: #000000;
  --rt-06-blue: #0000aa;
  --rt-06-cyan: #00aaaa;
  --rt-06-ltgrey: #aaaaaa;
  --rt-06-dkgrey: #555555;
  --rt-06-ltcyan: #55ffff;
  --rt-06-yellow: #ffff55;
  --rt-06-white: #ffffff;
  --rt-06-red: #aa0000;
  --rt-06-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
  --rt-06-bg: #c9ccd2;
  --rt-06-bg: oklch(84% 0.008 260);
  --rt-06-surface: #eef0f3;
  --rt-06-surface: oklch(95% 0.004 260);
  --rt-06-ink: #14171c;
  --rt-06-ink: oklch(19% 0.008 260);
  --rt-06-ink-2: #464e58;
  --rt-06-ink-2: oklch(42% 0.014 260);
  --rt-06-line: #9aa1ab;
  --rt-06-line: oklch(71% 0.012 260);
  --rt-06-acc: #1247c9;
  --rt-06-acc: oklch(45% 0.2 264);
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--rt-06-bg);
  color: var(--rt-06-ink);
  font-family: var(--rt-06-mono);
  -webkit-text-size-adjust: 100%;
}

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

@media (prefers-color-scheme: dark) {
  .rt-06 {
    --rt-06-bg: #101318;
    --rt-06-bg: oklch(18% 0.008 260);
    --rt-06-surface: #1a1e25;
    --rt-06-surface: oklch(24% 0.01 260);
    --rt-06-ink: #eef1f5;
    --rt-06-ink: oklch(96% 0.004 260);
    --rt-06-ink-2: #a3abb6;
    --rt-06-ink-2: oklch(74% 0.012 260);
    --rt-06-line: #39414c;
    --rt-06-line: oklch(35% 0.014 260);
    --rt-06-acc: #7fa8ff;
    --rt-06-acc: oklch(76% 0.13 264);
  }
}

.rt-06[data-theme="dark"],
.rt-06:has(.rt-06__theme-input:checked) {
  --rt-06-bg: #101318;
  --rt-06-bg: oklch(18% 0.008 260);
  --rt-06-surface: #1a1e25;
  --rt-06-surface: oklch(24% 0.01 260);
  --rt-06-ink: #eef1f5;
  --rt-06-ink: oklch(96% 0.004 260);
  --rt-06-ink-2: #a3abb6;
  --rt-06-ink-2: oklch(74% 0.012 260);
  --rt-06-line: #39414c;
  --rt-06-line: oklch(35% 0.014 260);
  --rt-06-acc: #7fa8ff;
  --rt-06-acc: oklch(76% 0.13 264);
}

@media (prefers-color-scheme: light) {
  .rt-06:has(.rt-06__theme-input:checked) {
    --rt-06-bg: #101318;
    --rt-06-bg: oklch(18% 0.008 260);
    --rt-06-surface: #1a1e25;
    --rt-06-surface: oklch(24% 0.01 260);
    --rt-06-ink: #eef1f5;
    --rt-06-ink: oklch(96% 0.004 260);
    --rt-06-ink-2: #a3abb6;
    --rt-06-ink-2: oklch(74% 0.012 260);
    --rt-06-line: #39414c;
    --rt-06-line: oklch(35% 0.014 260);
    --rt-06-acc: #7fa8ff;
    --rt-06-acc: oklch(76% 0.13 264);
  }
}

@media (prefers-color-scheme: dark) {
  .rt-06:has(.rt-06__theme-input:checked) {
    --rt-06-bg: #c9ccd2;
    --rt-06-bg: oklch(84% 0.008 260);
    --rt-06-surface: #eef0f3;
    --rt-06-surface: oklch(95% 0.004 260);
    --rt-06-ink: #14171c;
    --rt-06-ink: oklch(19% 0.008 260);
    --rt-06-ink-2: #464e58;
    --rt-06-ink-2: oklch(42% 0.014 260);
    --rt-06-line: #9aa1ab;
    --rt-06-line: oklch(71% 0.012 260);
    --rt-06-acc: #1247c9;
    --rt-06-acc: oklch(45% 0.2 264);
  }
}

.rt-06__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-06__theme {
  position: absolute;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
  z-index: 6;
}

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

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

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

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

.rt-06__stage {
  max-inline-size: 72rem;
  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: 1.1rem;
}

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

.rt-06__frame {
  min-inline-size: 0;
  background: #6d6f70;
  padding: clamp(0.5rem, 0.3rem + 1vw, 1rem);
  border-radius: 6px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -2px 0 rgba(0,0,0,0.4), 0 14px 30px -16px rgba(0,0,0,0.6);
}

.rt-06__screen {
  container-type: inline-size;
  min-inline-size: 0;
  overflow: hidden;
  background: var(--rt-06-blue);
  color: var(--rt-06-ltgrey);
  padding: 0;
  border: 2px solid #1b1d1f;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
}

.rt-06__menu {
  display: flex;
  align-items: center;
  min-block-size: 1.6em;
  padding-inline: 0.5ch;
  background: var(--rt-06-ltgrey);
  color: var(--rt-06-k);
  font-size: clamp(0.66rem, 0.55rem + 0.6vw, 0.85rem);
  min-inline-size: 0;
  overflow: hidden;
  white-space: nowrap;
}
/* CGA dark red (index 4) rather than bright red (index 12): 4.71:1 on the
   light-grey menu bar where bright red is only 3.34:1, and both are real
   entries in the same 16-colour palette. */

.rt-06__menu-hi {
  color: #800000;
  font-weight: 700;
}

.rt-06__menu-gap {
  inline-size: 2ch;
  flex: none;
}

.rt-06__menu-tail {
  margin-inline-start: auto;
  color: #303030;
  padding-inline-start: 1ch;
}

.rt-06__art {
  margin: 0;
  padding: 0.5em 0;
  font-family: inherit;
  white-space: pre;
  color: var(--rt-06-ltcyan);
  min-inline-size: 0;
  overflow: hidden;
  font-size: min(0.95rem, 2.02cqi);
  line-height: 1.16;
  text-align: center;
}

@supports not (font-size: 1cqi) {
  .rt-06__art {
    font-size: clamp(0.35rem, 1.6vw, 0.95rem);
  }
}

.rt-06__art--narrow {
  display: block;
}

.rt-06__art--wide {
  display: none;
}

@media (min-width: 52rem) {
  .rt-06__art--narrow {
    display: none;
  }

  .rt-06__art--wide {
    display: block;
  }
}

.rt-06__status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 2ch;
  min-block-size: 1.6em;
  padding-inline: 0.5ch;
  background: var(--rt-06-ltgrey);
  color: var(--rt-06-k);
  font-size: clamp(0.66rem, 0.55rem + 0.6vw, 0.85rem);
  min-inline-size: 0;
}

.rt-06__key {
  color: #800000;
  font-weight: 700;
}

.rt-06__status-tail {
  margin-inline-start: auto;
  color: #303030;
}

.rt-06__inspect,
.rt-06__pal {
  min-inline-size: 0;
  background: var(--rt-06-surface);
  border: 1px solid var(--rt-06-line);
  padding: clamp(0.8rem, 0.6rem + 0.8vw, 1.1rem);
  display: grid;
  gap: 0.7rem;
  align-content: start;
}

.rt-06__inspect-h {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--rt-06-ink-2);
}

.rt-06__cells {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1px;
  background: var(--rt-06-line);
  border: 1px solid var(--rt-06-line);
  min-inline-size: 0;
  font-size: clamp(0.8rem, 0.6rem + 1.4vw, 1.35rem);
}

.rt-06__cell {
  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  min-inline-size: 0;
  background: var(--rt-06-blue);
  color: var(--rt-06-ltcyan);
  line-height: 1;
}

.rt-06__cell--hi {
  color: var(--rt-06-yellow);
}

.rt-06__inspect-p {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.55;
  color: var(--rt-06-ink-2);
  min-inline-size: 0;
  text-wrap: pretty;
}

.rt-06__inspect-p code {
  background: var(--rt-06-bg);
  padding: 0.05em 0.3em;
}

.rt-06__pal-list {
  list-style: none;
  margin: 0;
  padding: 0;
  min-inline-size: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 7.5rem), 1fr));
  gap: 2px;
}

.rt-06__pal-list li {
  min-inline-size: 0;
  font-size: 0.68rem;
  padding: 0.4rem 0.5rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--rt-06-ink-2);
  border: 1px solid var(--rt-06-line);
}

.rt-06__pal-list li::before {
  content: "";
  inline-size: 16px;
  block-size: 16px;
  flex: none;
  background: var(--rt-06-c);
  border: 1px solid var(--rt-06-line);
}

@media (min-width: 56rem) {
  .rt-06__stage {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .rt-06__badge,
    .rt-06__frame {
    grid-column: 1 / -1;
  }
}
```

How this works

Text mode is not a font choice, it is an addressing model. A CGA or VGA text screen is a fixed array of cells — 80 across, 25 down — and each cell holds one character code plus one attribute byte for foreground and background. There is no position between cell 12 and cell 13. Every interface built on that hardware inherited the constraint, which is why DOS-era dialogs are built from box-drawing characters instead of lines: a line would need to live between cells, and there is no between.

ch is the unit that preserves the model. One ch is the advance width of the "0" glyph, which in a monospace face is the advance width of every glyph. Size the grid columns at 1ch and the layout can only ever land on cell boundaries. Use px or rem instead and the grid drifts out of phase with the text by a fraction of a character per column, which compounds into a visible shear about forty columns in.

Fitting 80 columns is a font-size problem, not a layout problem. The field width is fixed at 80 character advances, so the only free variable is the size of the advance. A container query unit solves this exactly: font-size: min(1rem, 2.05cqi) makes 80 columns fill the frame's inline size and no more. Below about 52rem the resulting size drops under legibility, so this demo switches to a 40-column field — the honest answer, because scaling an 80-column screen onto a phone produces something that is period-accurate and unreadable.

The CGA palette is sixteen defined values, not a mood. Eight low-intensity colours and eight high-intensity ones, specified as exact sRGB. Several authentic pairings fail modern contrast outright — the classic dark grey #555555 on blue #0000AA is under 2:1 — so this demo keeps those pairings for decorative chrome only and raises every body-text pair to a passing combination, which in practice means the light greys and yellow on the blue field.

Make it yours

  • Set --rt-06-cols: 40 to force the narrow field at any width; every measurement follows from that one number.
  • Change --rt-06-fg and --rt-06-bg to any two CGA entries to reskin the whole screen; the palette tokens are named for their original attribute values.
  • Swap the double-line frame characters for single-line ones by editing the frame rows — the geometry is unchanged because both occupy one cell.
  • Raise the maximum font size past 1rem for a large-display kiosk look; the field stays 80 columns and simply gets bigger.
  • Recolour the menu bar to the black-on-light-grey used by DOS Shell rather than the Turbo Vision blue by swapping two tokens.
  • Turn the cell inspector's grid lines off by removing its background gradient — the text layer does not depend on them.

Gotchas — read before shipping

  • An 80-column character field cannot fit a 320px viewport at a legible size. Either drop to 40 columns at a breakpoint or accept a sub-8px cell; this demo drops and says so on screen.
  • Sizing the grid in px instead of ch puts the cell boundaries out of phase with the glyph advances, and the error accumulates into visible shear across a wide row.
  • Not every monospace stack has code page 437 box-drawing glyphs at the same advance width. Test the frame characters in your fallback fonts, or the frame breaks alignment where the glyph substitutes.
  • Authentic CGA pairings such as dark grey on blue fail SC 1.4.3 badly. Keep them for chrome, raise body text, and never carry meaning in the low-intensity pair alone.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

container query units (cqi) set a Chrome 105 / Safari 16 / Firefox 110 floor for the auto-fitting field, with a @media-based font size as the fallback for engines without them. oklch() and the :has() theme toggle are still the higher floor at Firefox 121. The ch unit itself is Baseline since 2011 and the box-drawing characters are plain UTF-8, so the character grid works far further back.

Techniques used in this demo

Search CodeFronts

Loading…