Skip to content

Repository files navigation

monowind

Build text-based user interfaces (TUIs) on the web from ordinary HTML and Tailwind utility classes — plain or with any framework (React, Svelte, Solid, Vue, …).

Author ordinary HTML with Tailwind utility classes, wrap it in <mono-wind>, and it renders as a strict character grid — box-drawing borders, integer-cell geometry, monospace everything — while native links, buttons, inputs, focus, forms, and accessibility semantics stay fully intact.

<mono-wind>
  <div class="flex justify-between items-center min-h-5 px-1 border">
    <div>This will be on the left</div>
    <button>This will be on the right</button>
  </div>
</mono-wind>
┌──────────────────────────────────────────────────────┐
│                                                      │
│ This will be on the left   This will be on the right │
│                                                      │
└──────────────────────────────────────────────────────┘

Status: pre-1.0. The feature surface below works and is covered by unit, story, and visual-regression tests, but APIs and behavior can still change. Design docs live in .agents/architecture, .agents/specs, and .agents/plans.

What works

Layout — block, flex, and grid (subgrid and named areas included), multi-column (columns-*, balancing, spanners, column rules), tables (collapsed borders as shared box-drawing lattices), floats (float-*/clear-*, with text wrapping beside them), margins, text wrap, and gap decorations (rule-* separators with junction glyphs).

Paint — rounded corners (rounded-* picks a theme's corner glyphs), box shadows (shadow-* as shade glyphs), gradients (bg-linear-*, bg-radial-*, bg-conic-*, bg-clip-text), opacity and translucent colors (blended into the cells), visibility (invisible, a box keeping its space), and transforms and filters (rotate-*, scale-*, translate-*, blur-*, grayscale, backdrop-blur-*, … — the element's cells in a layer of their own, which the browser transforms).

Motion — CSS transitions and animations (animate-spin, animate-pulse, keyframe enters and exits) sampled onto the grid.

Scrolling and position — scroll containers (overflow-auto/-scroll) with native physics and engine-drawn TUI scrollbars (drag the thumb, or press the track to page), sticky positioning (table headers and columns included), and anchor positioning (anchor-name, position-area, anchor(), anchor-size(), position-try-fallbacks, position-try-order, position-visibility) that places a menu under its button in cells.

Top layer — popovers and modal dialogs (popover, showModal()) paint above everything, their backdrop: drawn beneath.

Interaction — one cell-precise renderer draws the grid while the light DOM stays the browser's own, so everything above is native behavior rather than a reimplementation. Drag-select the grid, or set <mono-wind select="text"> for a semantic text mirror; :hover and :active work on any element without breaking selection (see Pointer states).

Getting started

No build step — one script tag, and <mono-wind> does the rest:

<script src="https://unpkg.com/monowind/dist/cdn.js"></script>

With Vite — the plugin brings Tailwind with it, so there is nothing to configure:

npm install -D @monowind/vite

With your own Tailwind v4 build:

npm install monowind
@import "tailwindcss";
@import "monowind";
import { defineMonoWind } from "monowind";
defineMonoWind();

See packages/core/README.md for the engine and packages/vite/README.md for the plugin. The pnpm --filter @monowind/example-* dev lines under Development each run one of these setups end to end.

Themes

@monowind/themes ships class-scoped themes modeled on real systems — dos, dos-blue, c64, green-phosphor, amber, teletype, bbs: authentic palettes (every Tailwind color token quantized to the system's colors), period fonts, and era-correct border characters (border-double renders +=+ on a teletype and downgrades to single lines on a phosphor terminal). Try the theme switcher in the playground; details in packages/themes/README.md. Anyone can build a theme — it's one CSS file against the core theming contract.

Components

@monowind/ui adds accessible components — a menu, a listbox, a select, a combobox, a dialog, a popover, a tooltip — as Zag.js state machines wired to the grid: Zag runs the roles, the keyboard, typeahead, focus, and dismissal; the engine places each floating part against its trigger in cells, in the top layer above everything, flipped where the host leaves no room, and a listbox in the flow like any other box. Headless, so a component styled through the theme's tokens wears whatever theme its host does.

Write one as markup — <mono-menu> and its kin, attributes for props and events for callbacks, which any framework or none can render, and the whole answer where a framework has no Zag adapter (Solid 2, and any markup-first stack — htmx, Turbo, Alpine, a server's views):

<mono-menu placement="bottom-start">
  <button data-part="trigger" class="border px-1">File</button>
  <div data-part="positioner" popover="manual">
    <div data-part="content" class="border bg-clear">
      <div data-part="item" data-value="new" class="px-1">New</div>
    </div>
  </div>
</mono-menu>

Or take the components of your framework — Menu.Root in React, MenuRoot in Vue and Svelte — its hook, composable or create…, the mount on markup marked with data-part, or Zag's adapter for another framework with the package's props() and connect().

See packages/ui/README.md for the parts, states, placement, and the framework path, and packages/ui-react, packages/ui-vue, and packages/ui-svelte for theirs.

Ascii-art banners

@monowind/ascii adds <mono-ascii>: FIGlet/TOIlet banner text rendered on the grid, with the semantic string intact for screen readers; selecting over the banner selects the art itself. Fonts are per-module imports (or registerAsciiFont with your own .flf/.tlf data); SGR-colored fonts and the effect attribute (rainbow, metal) paint through theme-aware --mw-ansi-* tokens.

<mono-ascii font="small" class="text-emerald-400">monowind</mono-ascii>

44 clearly-licensed fonts ship with the package; see packages/ascii/README.md for setup per integration and the full font list.

QR codes

@monowind/qr-code adds <mono-qr>: the element's text as a scannable QR code packed into the grid's cells — half blocks where a cell is twice as tall as wide, so a version-1 code is 21 × 11 — with the value kept in the light DOM for screen readers and a drag over the code selecting characters that paste as a working code. Padding is its quiet zone; text-* and bg-* set its colors; glyph sets restyle its modules.

<mono-qr class="mx-auto px-2 py-1">https://play.monowind.benface.com</mono-qr>

See packages/qr-code/README.md for the attributes.

Pointer states in grid mode

Under the default select="grid", non-interactive elements pass pointer events through to the grid so drag-selection works — which would normally make :hover/:active dead on a plain <div>. monowind synthesizes both instead: the engine hit-tests the pointer against the cell layout and Tailwind's hover: and active: variants (plus group-*/peer-*) respond as usual, cursor-* included, with selection intact. Two things still need a real hit target: native title tooltips and your own JS click handlers on non-interactive elements — opt those elements in with pointer-events-auto! (they then block grid selection over their cells, like buttons do). The other way round, pointer-events-none works as it does natively: a badge laid over a button leaves the press to the button, and a link it disables (with aria-disabled and no href, which keeps it out of the keyboard's reach too) takes no press.

If you redefine Tailwind's hover: variant yourself, your definition wins — include the data attribute (and Tailwind's hover-capability gate) to keep grid-mode hover working:

@custom-variant hover {
  @media (hover: hover) {
    &:is(:hover:where(:not([data-mw-covered])), [data-mw-hover]) {
      @slot;
    }
  }
}

([data-mw-covered] marks an element another box paints over: the browser drops its hover with its pointer events, and the :where() drops the style at once where an engine lags.)

Structure

This is a monorepo managed with pnpm workspaces:

  • apps/ — applications (Storybook, the playground, example apps)
  • packages/ — the library packages (core engine, build integrations, elements, components)
  • .agents/ — working documents for AI agents (specs, plans, architecture)

Showcase & docs

  • Storybook — live examples of every supported feature, deployed from apps/storybook.
  • Playground — edit HTML in the browser and see the character grid update live; every document is a shareable URL, long or short. Deployed from apps/play.

Development

pnpm install

# Storybook (the main showcase / dev environment), port 6006
pnpm dev

# lint + format check + canonical Tailwind classes + typecheck
pnpm check

# same, but auto-fixes lint, format, and non-canonical class issues
pnpm check:fix

# tests (unit + golden + story tests + example smoke tests), one package
# at a time: the app tests that serve a package's bundle rebuild it
pnpm test

# visual regression tests (screenshots via Docker, one per story)
pnpm test:visual

# regenerate the screenshot baselines
pnpm test:visual:update

# build all packages
pnpm build

# interactively update dependencies across the workspace
pnpm check-updates

# Playground (live HTML editing through <mono-wind>, shareable URLs), port 5181
pnpm --filter @monowind/play dev

# same, wrapped in the Netlify CLI so the short-link functions run too, port 8888
pnpm --filter @monowind/play dev:netlify

# Example apps (each demonstrates one way to consume monowind):
pnpm --filter @monowind/example-html dev       # CDN mode: one script tag
pnpm --filter @monowind/example-tailwind dev   # your own Tailwind v4 build
pnpm --filter @monowind/example-vite dev       # standalone: @monowind/vite, zero Tailwind setup
pnpm --filter @monowind/example-react dev      # React 19 + @monowind/vite
pnpm --filter @monowind/example-solid dev      # Solid 2.0 (RC) + @monowind/vite
pnpm --filter @monowind/example-svelte dev     # Svelte 5 + @monowind/vite
pnpm --filter @monowind/example-vue dev        # Vue 3 + @monowind/vite

# Styled by something other than Tailwind — the engine reads computed
# styles, so what wrote them does not matter:
pnpm --filter @monowind/example-unocss dev          # UnoCSS
pnpm --filter @monowind/example-panda dev           # Panda CSS
pnpm --filter @monowind/example-vanilla-extract dev # vanilla-extract
pnpm --filter @monowind/example-stylex dev          # StyleX

# Driven from attributes, with @monowind/ui's elements and no
# component code of their own:
pnpm --filter @monowind/example-htmx dev       # htmx
pnpm --filter @monowind/example-alpine dev     # Alpine
pnpm --filter @monowind/example-turbo dev      # Turbo (Hotwire)
pnpm --filter @monowind/example-datastar dev   # Datastar

About

Text-based user interfaces (TUI) on the web, from ordinary HTML and Tailwind utility classes

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages