Skip to content
jeannesbryanPublic

About

A zero-JS, Tor-friendly CSS framework for building privacy-first cyberpunk and terminal-inspired interfaces.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

> TORMINAL CSS_ 📟

The Zero-JS, Tor-Friendly Terminal UI Framework

CSS3 Zero JS Tor Friendly No Dependencies

Torminal CSS is a lightweight, dependency-free CSS framework for building terminal-inspired interfaces that work cleanly in privacy-first environments such as Tor Browser. It uses native HTML and CSS only: no frontend JavaScript, no remote assets, no webfont downloads, and no build step.

Torminal is used as the UI foundation for DeadDrop, but it is designed to be reusable in other small PHP/static apps that need low-resource, keyboard-friendly, mobile-ready interfaces.

Live docs: docs.html walks every module below and previews it in place. index.html is the landing/showcase page. Both are single files with no <script> tag anywhere — open them directly from disk.


Design Goals

  • Zero JavaScript: interactive patterns rely on native HTML or CSS-only mechanisms such as :checked, :target, and <details>.
  • Tor-friendly: no external fonts, no remote CSS, no tracking dependencies, and a conservative default monospace stack.
  • Low-resource: one plain CSS file, no bundler, no runtime framework.
  • Readable on mobile: responsive containers, flex utilities, touch-friendly buttons, and stack-on-mobile helpers.
  • Accessible by default: visible keyboard focus, reduced-motion support, and CSS-only toggles that stay screen-reader and keyboard reachable.
  • Reusable: shared utility classes and modules replace page-level inline CSS.

The Tor-Safest Rules

These are not stylistic preferences. Each one removes a measurable fingerprinting, compositing, or accessibility cost, and each is enforced by a comment at the rule it applies to.

Rule Why
No @font-face, no font stacks. --t-font is the bare keyword monospace. Naming fonts — even system ones — makes glyph rendering depend on what the visitor happens to have installed. That is a machine-identifying signal. A generic keyword renders the same everywhere.
No backdrop-filter. Forces a GPU compositing layer, costs battery on low-end hardware, and buys nothing over a flat alpha wash.
No remote assets of any kind. No CDN, no @import, no webfont, no analytics. Nothing that could beacon.
No display: none on interactive inputs. Clipped radios/checkboxes stay in the tab order. display:none silently makes a JS-free widget keyboard-unusable.
Animations are neutralisable. Everything collapses under prefers-reduced-motion: reduce.
No JS-essential behaviour. If a feature cannot work without <script>, it is rebuilt on CSS primitives or left out — never faked.

Deliberately not shipped

These exist in the clearnet parent project and were not ported, because they cannot survive the rules above:

  • Live fuzzy-filter command palette — a jump menu of links replaces it.
  • Kanban drag-and-drop — the board ships as static layout only.
  • JS typewriter effect, TOTP live countdown, clipboard copy button, setTimeout-driven splash dismissal.
  • Every .active / .show / .is-open runtime toggle hook (they are unreachable without JS).

Quick Start

Drop torminal.css into your project and link it from the page header:

<link rel="stylesheet" href="assets/torminal.css">

Enable the CRT overlay by adding .t-crt to the body:

<body class="t-crt">
    <div class="t-container t-box-md mt-4">
        <h1 class="t-glow">&gt; HELLO_WORLD_</h1>
        <a href="#" class="t-btn">[ EXECUTE ]</a>
    </div>
</body>

The CRT effect is optional. For a calmer UI, use a plain <body> without .t-crt.


Core Utilities

A small utility layer, intentionally far smaller than Bootstrap:

  • Layout: .d-flex, .d-none, .flex-wrap, .flex-column, .justify-content-center/-between/-end, .align-items-center/-start, .gap-1 … .gap-4
  • Responsive display: .d-none-mobile, .d-block-mobile, .d-none-desktop, .d-block-desktop, .w-100-mobile, .text-center-mobile, .flex-column-mobile
  • Spacing: .m-0, .mt-*, .mb-*, .my-*, .p-*, .px-*, .py-* (.mt-5 / .mb-5 included)
  • Sizing: .w-25, .w-50, .w-75, .w-100, .w-auto, .h-100, .min-vh-100
  • Display / position: .d-block, .d-inline-block, .position-relative, .overflow-hidden, .overflow-auto
  • Auto margins: .ms-auto, .me-auto, .mx-auto
  • Text: .text-center/-right/-left, .text-muted, .text-success, .text-danger, .text-warning, .text-info, .text-white, .text-uppercase, .text-break, .fs-small, .fs-1/-2/-3, .font-bold, .fw-normal
  • Backgrounds: .bg-base, .bg-surface, .bg-grid
  • Borders: .t-border, .t-border-top, .t-border-bottom, .border-dashed, .border-danger, .border-warning, .border-success
  • Accessibility: .t-sr-only, :focus-visible, prefers-reduced-motion

Spacing utilities are !important, so a page can still override a utility with a more specific rule and a local stylesheet loaded after Torminal.


Theming

Two mechanisms, deliberately redundant so a theme never degrades into an unstyled fallback.

1. Attribute form — works in every engine. Put it on <html>:

<html data-terminal-theme="amber">

Presets: green (default), amber, cyan, red. Each reassigns the core palette tokens.

2. CSS-only radio switcher — requires :has() (Firefox 121+, Tor Browser 14.x+). It is wrapped in @supports selector(:has(*)), so older engines ignore the whole block and keep the attribute or default palette instead of breaking:

<div class="t-theme-switch">
    <label for="t-theme-green">green</label><input class="t-toggle-checkbox" type="radio" name="t-theme" id="t-theme-green" checked>
    <label for="t-theme-amber">amber</label><input class="t-toggle-checkbox" type="radio" name="t-theme" id="t-theme-amber">
</div>

The label must come before its input — the checked chip is highlighted with .t-theme-switch label:has(+ input:checked).

Coverage is total. A preset is seven tokens:

--bg-base  --bg-surface  --t-green  --t-green-dim  --t-accent-rgb  --t-code-fg  --t-white

--t-accent-rgb is the important one. Every translucent accent wash in the stylesheet — hover glows, tinted panels, dashed borders, the dot grid, focus rings, box shadows — is written as rgba(var(--t-accent-rgb), <alpha>) rather than a literal, so changing the theme recolours 100% of the surface instead of leaving green behind on a non-green theme. Legacy comma syntax is deliberate: rgba(var(--x), 0.05) works in every engine with custom properties, whereas color-mix() and the modern slash syntax do not.

The only fixed colours left are neutrals (#000, #fff, near-black overlays) and the semantic red / yellow / cyan status colours, which are theme-independent by design.

If you add a preset, set --t-accent-rgb to match --t-green — a mismatch shows up immediately as a green tint.


Components

Buttons

<a class="t-btn" href="#">[ TIMELINE ]</a>
<a class="t-btn ghost" href="#">[ SETTINGS ]</a>
<a class="t-btn info" href="#">[ RADAR ]</a>
<a class="t-btn warning" href="#">[ WARN ]</a>
<a class="t-btn danger" href="#">[ LOCK ]</a>
<button class="t-btn t-btn-sm">SMALL</button>
<button class="t-btn t-btn-block">FULL WIDTH</button>

Variants: default (green), .danger, .warning, .info, .ghost, plus .t-btn-sm and .t-btn-block.

Forms

<label class="t-form-label" for="key">Master Key</label>
<input class="t-input" id="key" type="password" placeholder="Insert Master Key...">
<input class="t-input error" type="text" value="bad value">
<span class="t-feedback text-danger">[!] Rejected.</span>
<textarea class="t-textarea" placeholder="Type payload..."></textarea>
<select class="t-select">
    <option>TTL: Forever</option>
</select>

Validation states: .t-input.error, .t-input.success, with .t-feedback for the message.

Terminal-style checkboxes and radios are CSS-only:

<label class="t-checkbox-label">
    <input type="checkbox" checked><span class="t-checkmark">Encrypt payload</span>
</label>
<label class="t-radio-label">
    <input type="radio" name="ttl" checked><span class="t-radiomark">Forever</span>
</label>

Cards, alerts, badges

<div class="t-card">
    <div class="t-card-header">[ Broadcast Station ]</div>
    <p>Terminal card content.</p>
</div>

<div class="t-card danger">
    <div class="t-card-header">[ Channel Compromised ]</div>
</div>

<div class="t-alert danger">[!] Authentication failed.</div>
<div class="t-alert warning">[~] Relay degraded.</div>
<div class="t-alert info">[i] Tor circuit rebuilt.</div>
<div class="t-alert private">[#] Private channel.</div>

<span class="t-badge">online</span>
<span class="t-badge private">private</span>
<span class="t-badge muted">idle</span>

Card variants: .danger, .warning. Badge variants: .danger, .warning, .info, .private, .muted. Alert variants: .warning, .danger, .info, .private.

Timeline / inbox posts

<div class="t-feed">
    <div class="t-post private">
        <div class="t-post-header">
            <span class="t-post-author">@alias</span>
            <span class="t-post-meta">ID: abc123</span>
        </div>

        <div class="t-quote private">
            <span class="t-quote-author">&gt; Replying to @node:</span>
            Previous signal preview...
        </div>

        <div class="t-post-content">Decrypted message content...</div>

        <div class="t-post-actions">
            <a class="t-btn t-btn-sm" href="#">[ REPLY ]</a>
        </div>
    </div>
</div>

Post variants: .local, .local-node, .private, .inbox, .burner, .deleted, .tombstone. Legacy page CSS that uses .post-card / .media-attachment still resolves — those are shipped as backward-compatible aliases.

Vault / lock panels

<div class="t-lock-panel private">
    <h1 class="t-vault-title t-glow">[ CLASSIFIED VAULT ]</h1>
    <div class="t-vault-subtitle">Authentication required.</div>
    <form method="post">
        <input class="t-input" type="password">
        <button class="t-btn t-btn-block font-bold">[ UNLOCK ]</button>
    </form>
</div>

Inspector: key/value, metrics, fingerprints

.t-kv accepts either semantic markup or the div form — both render identically:

<!-- semantic -->
<dl class="t-kv">
    <dt>host</dt><dd>ddeadrop.onion</dd>
    <dt>exit</dt><dd class="text-break">somerandomlongexitnodekey</dd>
</dl>

<!-- div aliases -->
<div class="t-kv">
    <div class="t-kv-row"><div class="t-kv-key">host</div><div class="t-kv-value">ddeadrop.onion</div></div>
</div>

Also available: .t-metric-grid / .t-metric / .t-metric-value / .t-metric-label, .t-fingerprint for long identifiers and onion URLs, .t-code / .t-code-inline / .t-kbd, and .t-copyable + .t-copy-btn for copy-button layout (there is no clipboard behaviour — that needs JS).

Status primitives

<span class="t-led-dot t-led-green"></span>      <!-- also -yellow, -red -->
<div class="t-progress-bg"><div class="t-progress-bar" style="width:62%">62%</div></div>
<div class="t-skeleton"></div>
<span class="t-spinner"></span>
<div class="t-toast-container"><div class="t-toast">saved</div></div>
<span class="t-typewriter-cursor"></span>
<div class="t-timer-line-bg"><div class="t-timer-line" style="width:40%"></div></div>

Progress and timer widths come from an inline style on a server-rendered value; no JS is involved in displaying a static number. Toast styling is provided, but triggering a toast is the host page's problem.

Chrome & layout

.t-app-shell (centred page wrapper), .t-toolbar + .t-toolbar-group, .t-statusline, .t-command (a $ … command-line block), .t-command-row + .t-command-prompt (an input row with a prompt), .t-breadcrumb, .t-shell + .t-shell-header/-title/-body (terminal window shell), .t-window, .t-pagination + .t-page-link / .t-page-current / .t-page-btn.

Hero / landing: .t-hero (add .compact for a shorter one), .t-hero-title, .t-hero-subtitle, .t-logo-frame, .t-logo-glow.

Content modules

  • Media: .t-media-grid, .t-media-item, .t-video-item, .t-media-frame, .t-media-caption
  • Social: .t-avatar / .t-avatar-lg, .t-spoiler, .t-feed-card family (-main, -header, -author, -meta, -content, -actions, -btn)
  • Composer: .t-editor-card, .t-editor-toolbar, .t-editor-btn, .t-editor-textarea
  • Prose: .t-markdown (headings, blockquote, pre, code, lists, images)
  • Activity: .t-activity-card family, .t-metric-box / .t-metric-val / .t-metric-large / .t-metric-unit / .t-metric-lbl
  • Layout: .t-board family (static — no drag), .t-masonry, .t-thread + .t-thread-item
  • Chat: .t-chat-container, .t-bubble, .t-bubble-them, .t-bubble-me, .t-bubble-meta
  • Bilingual text swap: .t-bilingual (or -btn / -tab) with .t-lang-default / .t-lang-alt — swaps on :hover, :focus and :focus-within

CSS-only Interaction Patterns

Everything below works with no JavaScript. Each one lists the markup constraint that makes it work, because in plain CSS those constraints are not optional.

Tabs and SPA panes

Both use the same primitive: a clipped radio immediately followed by its label, with the panels as later siblings of that radio.

<div class="t-tabs-wrapper">
    <input class="t-tab-radio" id="tab1" name="tabs" type="radio" checked>
    <label class="t-tab-label" for="tab1">Timeline</label>

    <input class="t-tab-radio" id="tab2" name="tabs" type="radio">
    <label class="t-tab-label" for="tab2">Inbox</label>

    <div class="t-tab-content-css" id="content1">Timeline content...</div>
    <div class="t-tab-content-css" id="content2">Inbox content...</div>
</div>

.t-pane is the same component under the clearnet name, wired as #pane1…#pane10 ↔ #paneview1…#paneview10.

Two constraints, both load-bearing:

  1. the label must be the radio's adjacent next sibling — that is what styles the selected tab;
  2. the panel must be a later sibling of the radio — that is what reveals it.

Wrapping the inputs in a separate header <div> satisfies (1) but silently breaks (2), because #tab1:checked ~ #content1 can no longer match. If you want that nested shape anyway, it works on engines with :has() (Firefox 121+ / Tor Browser 14.x+); Torminal ships a second rule inside @supports selector(:has(*)) for it.

Wiring covers 10 tabs/panes, which is the practical ceiling for a CSS-only strip: plain CSS cannot pair an arbitrary radio with the panel at the same index, so each slot needs its own selector — there is no loop and no wildcard. Slots 6–10 work today; to go beyond, extend all three selector lists in the stylesheet (the #tabN list, the #paneN list, and the @supports :has() form).

Positional mode — for pages with more than one strip

The id-based wiring above has a real constraint: the ids are global, so only one strip per page can use #tabN / #contentN. Any page needing a second strip would collide.

Add .t-tabs-stack and panels become <section> instead of <div>:

<div class="t-tabs-wrapper t-tabs-stack">
    <input class="t-tab-radio" id="x1" name="x" type="radio" checked>
    <label class="t-tab-label" for="x1">one</label>
    <input class="t-tab-radio" id="x2" name="x" type="radio">
    <label class="t-tab-label" for="x2">two</label>

    <section class="t-tab-panel">panel one</section>
    <section class="t-tab-panel">panel two</section>
</div>

The k-th radio now pairs with the k-th panel positionally, so ids are irrelevant and strips no longer collide — unlimited strips, 10 slots each. It works because the element types differ: :nth-of-type counts <input> for the radios and <section> for the panels, so any number of <div>/<label> siblings can sit between them without shifting either count. That is precisely why the panels must be <section> — a <div> would be counted against other divs in the wrapper.

The labels also do not have to be adjacent to their radios in this mode. Both modes can coexist on one page.

Modal (:target)

<a href="#help" class="t-btn">[ OPEN HELP ]</a>

<div id="help" class="t-modal">
    <div class="t-modal-content">
        <a href="#" class="t-modal-close-btn">X</a>
        <h2 class="t-card-header">[ Help ]</h2>
        <p>No JavaScript required.</p>
    </div>
</div>

Lightbox (:target)

<a href="#lightbox-1"><img src="thumb.jpg" alt=""></a>

<div class="t-lightbox" id="lightbox-1">
    <a class="t-lightbox-backdrop" href="#_" aria-label="Close"></a>
    <img src="full.jpg" alt="">
    <a class="t-lightbox-close" href="#_" aria-label="Close">[X]</a>
</div>

The absolutely-positioned backdrop anchor is what makes click-outside-to-close work without a script.

Command palette (:target)

<div class="t-command-palette" id="cmd">
    <a class="t-command-palette-backdrop" href="#_" aria-label="Close"></a>
    <div class="t-command-palette-box">
        <div class="t-card-header">jump to</div>
        <ul class="t-command-palette-list">
            <li><a class="t-command-palette-item" href="#">&#9656; home</a></li>
        </ul>
    </div>
</div>

This is deliberately a navigation menu, not a search box. The live fuzzy filter in the clearnet parent is genuinely JS-essential, so it was dropped rather than faked.

Accordion (native <details>)

<details class="t-accordion-native" open>
    <summary>Section</summary>
    <div class="t-accordion-body">Body text.</div>
</details>

File tree (native <details>)

<ul class="t-tree">
    <li><details open><summary>nodes</summary>
        <ul><li><a class="t-tree-file" href="#">alpha.onion</a></li></ul>
    </details></li>
</ul>

Dropdown, toggle switch, file input

  • .t-dropdown + .t-dropdown-label + .t-dropdown-menu + .t-dropdown-item, driven by a .t-toggle-checkbox sibling.
  • .t-switch + .t-switch-slider around an <input type="checkbox">.
  • .t-file-wrapper + .t-file-btn — a pure-CSS label overlay. The chosen filename cannot be echoed back, so the label text is static.

Docs Kit

docs.html is built from these; reuse them for your own manual:

.t-docs-body, .t-sidebar + .t-sidebar-logo, .t-main-content, .t-docs-section (add .no-border to drop the rule), .t-docs-example (renders a PREVIEW tag), .t-docs-code, .t-docs-utility-list, .t-docs-center-demo, .t-demo-strip, .t-demo-half, .t-demo-width-70, .t-demo-min-h, .t-clickable-demo.

Unprefixed aliases (.docs-section, .docs-example, .docs-code, .sidebar-logo) are kept for drop-in compatibility; the .t-docs-* spelling is canonical. There is a @media print block that flattens the docs layout to black-on-white.


Privacy Notes

Torminal is designed to avoid unnecessary fingerprinting surface:

  • Do not load remote fonts.
  • Do not import third-party CSS.
  • Do not add analytics snippets.
  • Prefer native HTML controls over custom JavaScript widgets.
  • Keep animation optional and honour prefers-reduced-motion.

The .t-crt overlay is aesthetic. It can be disabled if readability or performance is more important. Note that prefers-reduced-motion: reduce already suppresses both the CRT overlay and .t-glow.


Contributing

Torminal welcomes CSS-only components and utility improvements. The rule of thumb remains:

If it requires <script>, it does not belong in Torminal core.

Keep it simple, local, reusable, and Tor-friendly. When a feature cannot survive without JS, prefer rebuilding it on :checked, :target or <details> — and if that is not possible, leave it out and say so in the docs rather than shipping a dead control.

About

A zero-JS, Tor-friendly CSS framework for building privacy-first cyberpunk and terminal-inspired interfaces.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages