AsciiTheme is a framework-agnostic micro-package that adds an ASCII visual layer to existing pages. It provides:
data-style="default|ascii"management- optional mode management (
light|dark) - box-drawing sticker rendering for
[data-ascii-sticker]
Runtime has no dependencies.
Quick demo from the Liqua landing: switching light/dark, then ASCII mode, and light/dark inside ASCII.
For a more detailed walkthrough of theme toggles across integrated sites, see the video demo: https://youtu.be/VVQX7DtGHX0
dist/ is committed to git for CDN convenience and reproducible release snapshots.
npm install @abvx/ascii-themeimport { initAsciiTheme } from "@abvx/ascii-theme";
import "@abvx/ascii-theme/style.css";
initAsciiTheme();import { initAsciiTheme } from "@abvx/ascii-theme";
import "@abvx/ascii-theme/style.css";
initAsciiTheme({ managedMode: false, base: false });Import CSS once in app/layout.tsx or pages/_app.tsx:
import "@abvx/ascii-theme/style.css";Run init only on the client:
"use client";
import { useEffect } from "react";
import { initAsciiTheme } from "@abvx/ascii-theme";
export function AsciiThemeBoot() {
useEffect(() => {
initAsciiTheme();
}, []);
return null;
}<link rel="stylesheet" href="https://unpkg.com/@abvx/ascii-theme@0.2.0/dist/style.css" />
<script src="https://unpkg.com/@abvx/ascii-theme@0.2.0/dist/ascii-theme.umd.js"></script>
<script>
AsciiTheme.initAsciiTheme({ managedMode: false });
</script>If npm install is unavailable in a host project, use a pinned GitHub commit via jsDelivr:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/markoblogo/AsciiTheme@<commit>/dist/style.css" />
<script src="https://cdn.jsdelivr.net/gh/markoblogo/AsciiTheme@<commit>/dist/ascii-theme.umd.js"></script>Use a commit hash or release tag, not @main.
By default, the package does not control your host light/dark theme.
It reads the host theme attribute (data-theme by default):
<html data-theme="dark">import { initAsciiTheme } from "@abvx/ascii-theme";
initAsciiTheme({ managedMode: false });ASCII palette mapping uses host theme selectors:
:root[data-style="ascii"][data-theme="light"]:root[data-style="ascii"][data-theme="dark"]
If you want the plugin to control mode itself:
import {
initAsciiTheme,
toggleAsciiMode,
} from "@abvx/ascii-theme";
initAsciiTheme({ managedMode: true, defaultMode: "light" });
document.getElementById("mode-btn")?.addEventListener("click", () => {
toggleAsciiMode();
});Managed mode uses data-ascii-mode="light|dark" on :root.
Use this when the host site has no built-in theme switch and you want the plugin to mount compact controls in a header container:
import { initAsciiTheme } from "@abvx/ascii-theme";
initAsciiTheme({
managedMode: true,
defaultMode: "dark",
defaultStyle: "default",
addThemeToggle: true,
addStyleToggle: true,
mountSelector: "header .right",
mountPlacement: "append",
className: "header-button",
});Notes:
- Toggles are injected only when
mountSelectoris provided and toggle flags are enabled. - Theme toggle switches
dark/light; style toggle text switchesASCII/Default. - ASCII style is manual-only by default on overlay integrations: new visits start in
default, and ASCII activates only after explicit user toggle. - When
base: true, style toggle is automatically disabled anddata-styleis forced toascii.
Use integrateTheme to control how mode is handled:
integrateTheme: "respect": always use host theme; never inject plugin theme toggle.integrateTheme: "managed": plugin controlsdata-ascii-modeand can inject both toggles.integrateTheme: "auto"(default): if host theme is detected, plugin switches to respect mode and disables injected theme toggle automatically.
For sites that already have their own light/dark switch:
initAsciiTheme({
integrateTheme: "auto",
addThemeToggle: true, // auto mode disables this when host theme is detected
addStyleToggle: true,
mountSelector: ".header-controls",
});For sites without host theme:
initAsciiTheme({
integrateTheme: "managed",
managedMode: true,
defaultMode: "dark",
addThemeToggle: true,
addStyleToggle: true,
mountSelector: ".header-controls",
});For cross-project consistency, follow the standard integration smoke-check:
- Copy
templates/theme-smoke-check.mjsinto your host project and run it asnpm run smoke:theme. - Apply the 4-state visual checklist from
docs/integration-smoke-check.md(default/asciixlight/dark). - Use
integrateTheme: "respect"for sites that already own light/dark; useintegrateTheme: "managed"only for sites without host theme controls.
- Host site already has light/dark theme + toggle:
use
integrateTheme: "auto", keep host theme toggle, add only ASCII toggle (addStyleToggle: true). - Host site has no theme system:
use
base: true,managedMode: true,addThemeToggle: true,addStyleToggle: false. - Always keep media untouched by default (
img/video/logo/avatar): no global media overrides.
- Publish package to npm.
- Update integrations to the new package/version (
@abvx/ascii-theme@x.y.z). - Run integration smoke-check (
default/light,default/dark,ascii/light,ascii/dark). - Verify no legacy links remain (
@markoblogo/...or old jsDelivr GH pins).
Use this order to avoid contrast regressions on utility-heavy sites:
- Start with
integrateTheme: "auto"andaddThemeToggle: true+addStyleToggle: true. - If no host theme is detected, switch to managed mode (
integrateTheme: "managed",defaultMode: "dark"). - Run the 4-state smoke check (
default/light,default/dark,ascii/light,ascii/dark). - If contrast is still weak in managed mode, rely on built-in readability hardening for common
text-*/bg-*/border-*utility classes before adding site-local CSS. - Add site-local bridge CSS only for truly project-specific tokens that cannot be generalized.
- Hardcoded Tailwind arbitrary tokens (for example
text-[#111827],bg-[#F9FAFB],border-[#E5E7EB]) are normalized by default in managed dark and ASCII modes.
When you want an ASCII-first site with no separate style axis, use the base preset.
In this mode ASCII is always on (data-style="ascii"), so you only keep the light/dark toggle.
import { initAsciiTheme } from "@abvx/ascii-theme";
import "@abvx/ascii-theme/base.css";
initAsciiTheme({
base: true,
managedMode: true,
addThemeToggle: true,
addStyleToggle: false,
mountSelector: ".header-controls",
});Import base CSS once in app/layout.tsx or pages/_app.tsx:
import "@abvx/ascii-theme/base.css";Run init in a client component (initAsciiTheme must run client-side):
"use client";
import { useEffect } from "react";
import { initAsciiTheme } from "@abvx/ascii-theme";
export function AsciiThemeBoot() {
useEffect(() => {
initAsciiTheme({
base: true,
managedMode: true,
addThemeToggle: true,
addStyleToggle: false,
mountSelector: ".header-controls",
});
}, []);
return null;
}CDN (pinned):
<link rel="stylesheet" href="https://unpkg.com/@abvx/ascii-theme@0.2.0/dist/base.css" />
<script src="https://unpkg.com/@abvx/ascii-theme@0.2.0/dist/ascii-theme.umd.js"></script>
<script>
AsciiTheme.initAsciiTheme({
base: true,
managedMode: true,
addThemeToggle: true,
addStyleToggle: false,
mountSelector: ".header-controls",
});
</script>Base preset is ASCII-only by design, so there is no style toggle in this mode. Keep the light/dark toggle enabled.
<link rel="stylesheet" href="https://unpkg.com/@abvx/ascii-theme@0.2.0/dist/base.css" />
<script src="https://unpkg.com/@abvx/ascii-theme@0.2.0/dist/ascii-theme.umd.js"></script>
<header class="a-container a-section a-cluster a-between">
<strong>ASCII Landing</strong>
<nav class="a-cluster"><a href="#">Docs</a><a href="#">GitHub</a></nav>
<div id="theme-controls"></div>
</header>
<main class="a-container a-stack a-gap-3">
<section class="a-section a-split a-gap-3">
<div class="a-stack a-gap-2">
<h1 class="a-balance">Ship a terminal-style landing in minutes.</h1>
<p class="a-prose a-muted">Use base.css + utilities only, no extra framework CSS.</p>
<div class="a-cluster a-gap-2"><a class="a-btn--primary" href="#">Start</a><a class="a-btn--ghost" href="#">Read docs</a></div>
</div>
<aside class="a-card">Sidebar panel</aside>
</section>
</main>
<script>
AsciiTheme.initAsciiTheme({
base: true,
managedMode: true,
addThemeToggle: true,
addStyleToggle: false,
mountSelector: "#theme-controls"
});
</script>Core token contract (base preset):
--bg, --fg, --muted, --border, --link, --code-bg, --radius, --pad, --gap, --container, --line, --font, --focus.
Light/dark mapping in managed mode is applied via:
:root[data-ascii-mode="light"](dark blue on white):root[data-ascii-mode="dark"](terminal green on black)
.a-split: 1 column on mobile, 2 equal columns at>=768px..a-aside: 1 column on mobile, content + sidebar (minmax(0, 1fr) 320px) at>=768px..a-aside--reverse: flips.a-asidecolumn order at>=768px..a-cluster: inline wrapped row withgap: var(--gap)and centered alignment.
| Class | Purpose | Notes |
|---|---|---|
.a-container |
Constrains page width and adds horizontal padding. | Centered with auto margins. |
.a-section |
Vertical spacing for page sections. | Uses --pad for subtle rhythm. |
.a-stack |
Vertical flex layout with configurable gap. | Uses --a-gap (default from --gap). |
.a-row |
Horizontal flex layout with wrapping and configurable gap. | Uses --a-gap (default from --gap). |
.a-cluster |
Inline row layout for nav/tags/footer links. | Wrapped + aligned center. |
.a-gap-1 |
Small gap helper. | Sets --a-gap: 8px. |
.a-gap-2 |
Default gap helper. | Sets --a-gap: 12px. |
.a-gap-3 |
Large gap helper. | Sets --a-gap: 20px. |
.a-center |
Centers text alignment. | text-align: center. |
.a-right |
Right-aligns text. | text-align: right. |
.a-between |
Distributes flex items across available space. | justify-content: space-between. |
.a-align-center |
Centers flex items on cross axis. | align-items: center. |
.a-wrap |
Forces flex wrapping. | Useful for compact control groups. |
.a-grid |
Base grid container with configurable gap. | Uses --a-gap (default from --gap). |
.a-cols-2 |
Responsive 2-column grid. | 1 column by default, 2 columns at >=768px. |
.a-cols-3 |
Responsive 3-column grid. | 1 column by default, 3 columns at >=768px. |
.a-cols-auto |
Auto-fit card grid. | repeat(auto-fit, minmax(220px, 1fr)). |
.a-span-2 |
Expands an item across two columns. | Applies at >=768px. |
.a-split |
Two-panel layout primitive. | Becomes two equal columns at >=768px. |
.a-aside |
Content + sidebar primitive. | Uses 1fr + 320px at >=768px. |
.a-aside--reverse |
Reversed content + sidebar primitive. | Flips .a-aside columns at >=768px. |
.a-prose |
Readable text measure and spacing. | max-width: 65ch + increased line-height. |
.a-muted |
Secondary text color. | Uses var(--muted). |
.a-card |
Terminal card surface. | Border + padding + transparent background. |
.a-panel |
Larger panel surface. | Same visual treatment as .a-card. |
.a-btn |
Base button style. | Border-only terminal button. |
.a-btn--primary |
Emphasized button style. | Filled with foreground color. |
.a-btn--ghost |
Secondary button style. | Transparent button with border. |
.a-badge |
Compact badge/pill style. | Border + small padding. |
<main class="a-container a-stack a-gap-3">
<section class="a-section a-split a-gap-3">
<div class="a-stack a-gap-2">
<h1>Ship faster with AsciiTheme</h1>
<p class="a-prose a-muted">Base preset gives you typography, layout, and controls with one stylesheet.</p>
<div class="a-cluster a-gap-2">
<a class="a-btn--primary" href="#">Start</a>
<a class="a-btn--ghost" href="#">Read docs</a>
</div>
</div>
<aside class="a-panel">Sidebar panel</aside>
</section>
<section class="a-section a-grid a-cols-3 a-gap-2">
<article class="a-card">Feature A</article>
<article class="a-card">Feature B</article>
<article class="a-card">Feature C</article>
</section>
</main>- Style axis is applied to root by the plugin:
data-style="default|ascii"
- Stickers:
[data-ascii-sticker="TEXT"]
- Optional role hooks for terminal components:
[data-ascii-role="cta"][data-ascii-role="card"][data-ascii-role="nav"][data-ascii-role="badge"]
initAsciiTheme(options?)(base: trueenables ASCII-only base preset)setAsciiStyle(style: "default" | "ascii")toggleAsciiStyle()getAsciiStyle(): "default" | "ascii"setAsciiMode(mode: "light" | "dark")toggleAsciiMode()renderAsciiStickers(root?: ParentNode)
- Contrast defaults in ASCII mode are tuned for utility-heavy sites (Tailwind-like
text-*/bg-*classes), so text and controls remain readable in both light and dark. - Card/panel classes (
.card,.card-gloss, and commoncard-*/panel-*variants) are normalized in managed dark and ASCII modes to prevent white-card regressions on dark backgrounds. - If your host theme uses a root
.dark/.lightclass (instead ofdata-theme), ASCII palette detection is supported out of the box. - This is not a full DOM-to-ASCII renderer.
- It focuses on a scoped theme layer and simple ASCII widgets (stickers + terminal component hooks).
- CSS is scoped to
:root[data-style="ascii"]to avoid host-site breakage. - Media is not restyled by default in ASCII mode (
img/video/avatar/logoremain unchanged unless you style them explicitly).
npm install
npm run devOpen the URL shown by Vite.
Live demo (GitHub Pages): https://markoblogo.github.io/AsciiTheme/
In the wild:
- You can also see this theme in the wild on the AGENTS.md generator landing: https://agentsmd.abvx.xyz/
- You can also see this theme on go.abvx.xyz: https://go.abvx.xyz/
- You can also see this theme on abvx.xyz: https://abvx.xyz/
- You can also see this theme on trade-solution.eu: https://trade-solution.eu/
- First-time setup: in GitHub repository settings, set Pages -> Source to GitHub Actions once, then rerun the Pages workflow.
To build the package:
npm run buildBefore tagging:
git status --short
npm ci
npm run build
npm run demo:buildCreate and push release tag:
# version is already 0.1.0 in package.json
git add -A
git commit -m "chore: release v0.1.0"
git tag v0.1.0
git push origin main --tagsManual npm publish (requires ownership and credentials):
npm login
npm publish --access publicIf package is unscoped, use:
npm publish