Skip to content
Lokta An editorial UI system · v0.2
光の写本 · LOKTAWCAG 2.2 AA

After the Asian manuscript page

Lokta

The screen as a printed page. Named after Nepali lokta paper, drawn from one trans-Asian paper tradition: Japanese washi, Himalayan lokta, and the Bengali Pala manuscripts. A warm cream stock, hatched breath rules, grotesk titles run hard to the margin, pigment grounds. Every text role clears WCAG 2.2 AA.

Type

Archivo, Spline Sans Mono, Source Serif 4, Noto Sans JP, Anek Bangla, Mukta, Martel, and Datatype. All SIL OFL, self-hosted, with each font's licence in fonts/NOTICE.md.

Color

Warm paper surfaces, warm-tinted ink text, saturated pigment grounds. Marigold is the hero.

Stocks

Paper, Ink, Bone, Indigo. Every text role clears AA on each.

Tokens

Three tiers (primitives, semantic, stocks) built with Style Dictionary to CSS, SCSS, and JS.

Install

Three ways in. Set the stock with data-theme on <html> (default is paper).

npm

npm install @lokta/tokens @lokta/css
@import "@lokta/tokens/css/lokta.css";
@import "@lokta/css/lokta.css";

Standalone repos

npm install github:msradam/lokta-css
npm install github:msradam/lokta-marp
npm install github:msradam/lokta-typst
npm install github:msradam/lokta-mermaid

Each repo is self-contained with its own quick start.

Drop-in (no build)

<link rel="stylesheet"
  href="https://msradam.github.io/lokta/lokta.css">

The site lokta.css bundles the tokens, base, components, and utilities in one link.

Components reference Patterns gallery Source on GitHub

Customization

Opinion lives in what cannot be changed. Lokta exposes six brand dials, each range-limited so even at its extreme the output is recognizably Lokta. Everything else (the type scale, the 8px grid, the AA rules, the flat hard-edged character) is locked.

The six dials

DialRange (the guardrail)
Stocka curated set (paper, manuscript, bone, ink, indigo, highland, slate, steel, onyx, and light variants), not a freeform background
Accenta curated pigment (marigold, madder, lac, cinnabar, indigo, …), not a colour picker
Voicenamed typeface options per role (Archivo or Mukta for display, Source Serif or Martel for serif). Script is automatic by language, never a dial
Densitycomfortable or compact. Two steps
Radiusclamped 0 to 3px. Square is the default and the ceiling is gentle
Grainoff, subtle, or fibrous. Texture, never depth

Why six, and why range-limited

Disciplined systems converge on three to eight consumer dials, clustering at five to six. The strength is in the constraints: every dial is range-limited rather than freeform, the font work is one Voice dial rather than several inputs, and script is an automatic context, not a preference.

SystemConsumer dials
Radix Themesaccent, gray, appearance, radius, scaling, panel (about 6)
Material 3a seed colour plus light/dark (about 1 to 2)
USWDScolour families, spacing base, type scale, font families (a handful)
LoktaStock, Accent, Voice, Density, Radius, Grain (6)

If a brand needs more, the answer is a new curated option inside an existing dial (a new stock, a new accent, a new Voice option), or a component token, never a new knob. Full philosophy in CUSTOMIZATION.md.

Foundations

Color

Never pure white, never pure black. Contrast ratios are noted against paper-01. Every text role clears WCAG 2.2 AA on its surface in every stock.

Paper · surfaces
paper.00 #FAF8EA deckle, top sheet
paper.01 #F4F1DF page, primary surface
paper.02 #EAE6D2 page shadow, sidebar
paper.03 #DBD3BB inset, table stripe
paper.04 #C2B89C divider, plate edge
Ink · text
ink.20 #B8B0A1 1.9:1, hairlines/fills only, never text
ink.40 #8E867A 3.1:1, borders/large/disabled only
ink.50 #615A4C 4.9:1, muted text (AA)
ink.60 #5C564B 6.3:1, secondary (AA)
ink.80 #2A2620 13.1:1, body
ink.90 #1F1C13 13.9:1, headlines
ink.100 #16140E 14.8:1 on paper-01
Pigment · grounds
pigment.aubergine #6B4E8E
pigment.marigold #FBBC0E hero feature ground, requires dark text
pigment.peach #E7A079 heritage salmon (Kabir cookbook), dark text
pigment.lavender #A99CB3 cover / brand tone, dark text
pigment.night #070D0E dramatic section-opener ground
pigment.cinnabar #C23A26
pigment.celadon #6E8B6F
pigment.indigo #2E3E5C
pigment.celadon-ink #4F6B50 5.2:1, success text on paper
pigment.cinnabar-ink #C23A26 4.7:1, danger text on paper
pigment.aubergine-ink #6B4E8E 4.6:1, feature text on paper
Stocks
Paper · light, default

Headline primary

Body text on the page surface, AA across every role.

Secondary · muted

Done Alert
Ink · warm dark

Headline primary

Body text on the page surface, AA across every role.

Secondary · muted

Done Alert
Bone · cool light

Headline primary

Body text on the page surface, AA across every role.

Secondary · muted

Done Alert
Indigo · cool dark

Headline primary

Body text on the page surface, AA across every role.

Secondary · muted

Done Alert

Type

A real type set: every size pairs a line-height, weight, and tracking.

RoleTokenSize / LHWeightTrackingUse
Display --type-3xl 72 / 1.05 800 -0.03em Book cover
Section --type-2xl 48 / 1.05 700 -0.03em Section opener
Title --type-xl 32 / 1.2 700 -0.01em Recipe title
Subhead --type-lg 24 / 1.2 600 -0.01em Deck
Lead --type-md 18 / 1.45 400 0 Lead-in
Body --type-base 15 / 1.45 400 0 Paragraphs
Caption --type-sm 13 / 1.45 400 0 Meta, captions
Label --type-xs 11 / 1.2 500 0.12em Tracked mono label
Display / Body

Archivo. A neutral editorial grotesk.

Mono

Spline Sans Mono 0123

Serif

Source Serif 4 pull quote

CJK

光の写本

Bengali

রান্না খাদ্য পুষ্টি

Spacing

An 8px grid with a 4px half-step. Generous gutters, paper measure.

space-1 4px
space-2 8px
space-3 12px
space-4 16px
space-5 24px
space-6 32px
space-7 48px
space-8 64px
space-9 96px

Grid

Twelve columns, 24px gutters (space-5), on an 8px base. Body measure caps near 72ch for readability. Breakpoints: 480 · 768 · 1024 · 1440.

Motion

Paper does not bounce. Productive easing for feedback, expressive for entrances. Honors prefers-reduced-motion.

TokenValueUse
--ease-papercubic-bezier(0.2, 0, 0.1, 1)UI feedback
--ease-productivecubic-bezier(0.2, 0, 0.38, 0.9)State changes
--ease-expressivecubic-bezier(0.4, 0.14, 0.3, 1)Entrances
--dur-fast120msHover, press
--dur-base200msMost transitions
--dur-slow320msOverlays

Icons

Tabler as the base, sharpened: square line caps, miter joins, 2px stroke, currentColor. Self-hosted as a vendored sprite (npm run build:icons); Myna UI is the alternative set. The live searchable browser is on the components reference.

Voice and content

The screen is a printed page, so the words follow the source cookbook's editorial grammar (Cuisine on Screen, Sachiyo Harada, Prestel). Precise, calm, and encouraging; instructive without being terse; international (imperial and metric, both); and never marketed. A deterministic linter (validate/content.mjs) holds the rules that can be checked.

The rules, from the book

ElementRuleFrom the book
Titles and headingsTitle Case, minor words lower. Spell out "and", never "&". Oxford comma.Soups, Stews, and Noodles
Labels and metadataSentence case with a colon; units spelled out.Preparation time: 3 minutes
QuantitiesReal fractions, imperial first with metric in parentheses, tabular figures (Datatype and .lk-frac).2 1/2 cups (600 ml) water
Instructions and buttonsImperative, verb first; calm and precise, with reassuring asides.Stir with a wet wooden spatula.
Asides and tipsShort footnotes, practical and warm. Em dashes are welcome.* Set a timer—it is practical.
ToneNever sell, never hype. The gate bans the marketing words.The book never markets a recipe.

Microcopy

Errors state what happened and what to do, calmly and without blame, the way the recipe reassures ("it will be translucent at first"). Empty states name what will appear and the one action to fill it. Confirmations are quiet and specific. Link text says where it goes, never "click here". Inclusive terms throughout: allowlist and blocklist, primary and replica.

Motion and data

Two additive layers that ship inside lokta-css. The motion layer is a flat, accessibility-first reveal vocabulary where reduced motion is the floor, not an afterthought. Datatype sets charts as type, inline in a sentence.

Datatype · charts in the sentence

Weekly actives climbed {l:20,45,60,55,80,95} through the quarter, error rate held flat {l:8,6,7,5,6,4}, and the region split {b:62,24,14} stayed steady. Conversion sits at {p:62} of target.

Datatype is a variable OpenType font by Frank Tisellano (SIL OFL 1.1; it embeds IBM Plex Mono glyphs, Reserved Font Name "Plex"). Ligature substitution renders {b:…} bars, {l:…} sparklines, and {p:…} pie, no SVG and no script, so the same text renders identically in a page, a slide, and a Typst PDF. Every chart is role="img" with an aria-label that states the trend in words; lokta-chart.js emits the source and the label together so they cannot drift.

Motion · five flat primitives

rule-in, set-in, leaf-turn, stamp, and write-in, each a manuscript or kitchen gesture, each flat by construction: no opacity fade, no blur, no scale-bloom. Tier 1 keeps a static equivalent under reduced motion; Tier 2 is removed entirely. The live primitives, the streaming-response pattern, and the persisted reduce-motion toggle are on the components reference.

Kolam · woven line ornaments

A sikku kolam, one continuous line woven around a grid of pulli, the alpana tradition behind the Bengali cookbook lineage. lokta-kolam.js generates it deterministically as pure SVG (so it prints in Typst too), themes it through currentColor, binds the stroke to the rule scale, and can let the line write itself in via draw(). Every kolam is role="img" with a label.

Line-art tracing

Ink on paper
Marigold on ink

The image arm, in the cookbook's outline idiom: scripts/build-trace.mjs traces an image into vector contours that stroke in currentColor, so the same line drawing themes with the stock and prints in Typst. It is a deliberate treatment, not a mandate; full-colour images stay welcome. A line-and-flat-region source like a woodblock print traces cleanest. Source: Utagawa Hiroshige, Blue Bird and Hibiscus, The Metropolitan Museum of Art, CC0.

Recipe notation

2 1/2cups (600 ml) water
3/4tsp fine salt
1 1/3cups bread flour
45g caster sugar

Add 1/2 cup dashi, simmer 2 1/2 hours, then reduce by 1/3 before plating.

Quantities set the way a cookbook does. lokta-recipe.js wraps each bare N/M in a scoped .lk-frac so the OpenType frac feature renders a true fraction without superscripting the whole number or mangling the parenthetical, and .lk-qty aligns the column in tabular figures. No new font; it uses features already in the type.

Components

Built on the semantic layer, so they theme with the switcher above. Use it to feel every stock.

Open the components, icons, and accessibility reference

The reference page is keyboard-operable (tabs, accordion, dialog, menu) with the ARIA wiring from lokta-behaviors.js, and a live icon browser. It is what the Playwright and axe suite tests.

Buttons

Printed keys. 36px minimum target, square caps, optional radius via --lk-radius.

Tags

Hard-cornered metadata pills.

Outline Filled Pigment

Inputs

Text, select, textarea, with placeholder and disabled states.

Checkbox and radio

Square caps; the radio reads with an inner filled square.

Tabs

Live: click or use Left/Right, Home/End. Roving tabindex, real ARIA.

The overview panel.

Accordion

Live: Enter or Space toggles each panel (aria-expanded + region).

A stock re-points the semantic layer: Paper, Ink, Bone, Indigo.

Menu

Live: ArrowDown opens, arrows move, Escape closes and restores focus.

Dialog

Live: opens with focus trap, Escape closes, focus returns to the trigger.

Inline notifications

Color is paired with a glyph, so meaning never relies on hue.

Saved
The page was written to the press.
Failed
The plate did not register.
Note
Marigold demands dark text.

Pagination

Progress

Slider

Tooltip

Hover or focus the button.

Static tooltip

Status

Done Alert Pending

Code

npm install @lokta/tokens @lokta/css

Inline --surface-page too.

Data table

Tracked mono headers, hairline rules, striped rows, tabular figures.

StockSurfaceRoles
Paperpaper-0112
Inkink-9012
Indigo#1B223012

Modal

The one shadow in the system: a single hard offset, no blur.

Set the stock

Choose a paper for the run. The choice re-points every semantic token.

Editorial marks

Rules, the measured rule, and the hatched end-mark.




Page furniture

Running head, colophon, folio.

Chapter · Stocksp. 12
LoktaSet in Archivo and Spline Sans Mono
012

Examples

Whole pages built only from Lokta classes, each part of the axe-core suite. Open one, or copy its markup as a starting point.

Diagrams

The same diagram theme renders live in the browser and pre-renders to SVG for print and Typst. Square nodes, 1.5px ink strokes, straight edges, Archivo labels, Spline Sans Mono edge labels. Load a token theme and the colours track the active stock.

Lokta-themed flowchart: Intake to Validate to a Dedupe decision, then to an event store or a drop.

Node classes

ClassPigmentUse
heromarigoldthe one node to read first
storeceladona datastore
decindigoa decision
dangercinnabara failure or drop
mutedpapersecondary

Use it

// web
import mermaid from "mermaid";
import { initLoktaMermaid } from "@lokta/mermaid";
initLoktaMermaid(mermaid);

// print / Typst
mmdc -c lokta-mermaid.json -C lokta-mermaid.print.css -i d.mmd -o d.svg

Zero install on the web: import https://msradam.github.io/lokta/lokta.mermaid.mjs and the theme JSON beside it.

Documents

The print arm of the system: Typst document themes that carry the same cream stock, hatched rules, mono labels, and right-aligned grotesk titles onto the page. Built with the vendored static fonts.

TemplateWhat it is
lokta-techWhite technical report
lokta-reportCream editorial report
lokta-articleLong-form editorial
lokta-bulletinSingle-sheet notice
lokta-letterCorrespondence
lokta-coverPigment ground with the vertical spine
lokta-recipeAfter the cookbook page

Use it

#import "@local/lokta:0.1.0": *
#show: lokta-recipe.with(title: "Dashi Broth", film: "Spirited Away", ..)

Deck

The Lokta Marp theme renders this brief as slides, with the same fonts and pigments.

The deck is rendered into this site by the Pages workflow. To preview locally, run npm run build:deck and copy deck.html plus lokta-deck.pdf into site/.

Tokens reference

Generated from tokens/lokta.tokens.json. References like {ink.90} resolve through the semantic layer at build time. The values are checked on every push by npm run verify: WCAG AA contrast for every text role on every surface in every stock, cross-surface parity (the Typst and Mermaid literals equal their primitive), and the 8px grid.

View the verification dashboard

primitives

TokenTypeValueNotes
paper.00 color #FAF8EA deckle, top sheet
paper.01 color #F4F1DF page, primary surface
paper.02 color #EAE6D2 page shadow, sidebar
paper.03 color #DBD3BB inset, table stripe
paper.04 color #C2B89C divider, plate edge
ink.20 color #B8B0A1 1.9:1, hairlines/fills only, never text
ink.40 color #8E867A 3.1:1, borders/large/disabled only
ink.50 color #615A4C 4.9:1, muted text (AA)
ink.60 color #5C564B 6.3:1, secondary (AA)
ink.80 color #2A2620 13.1:1, body
ink.90 color #1F1C13 13.9:1, headlines
ink.100 color #16140E 14.8:1 on paper-01
pigment.aubergine color #6B4E8E
pigment.marigold color #FBBC0E hero feature ground, requires dark text
pigment.peach color #E7A079 heritage salmon (Kabir cookbook), dark text
pigment.lavender color #A99CB3 cover / brand tone, dark text
pigment.night color #070D0E dramatic section-opener ground
pigment.cinnabar color #C23A26
pigment.celadon color #6E8B6F
pigment.indigo color #2E3E5C
pigment.celadon-ink color #4F6B50 5.2:1, success text on paper
pigment.cinnabar-ink color #C23A26 4.7:1, danger text on paper
pigment.aubergine-ink color #6B4E8E 4.6:1, feature text on paper
dark-accent.success color #8FB088 6.7:1 on ink page
dark-accent.danger color #E2654F 4.8:1 on ink page
dark-accent.feature color #A98FC9 7.5:1 on ink page
type-scale.xs dimension 11px
type-scale.sm dimension 13px
type-scale.base dimension 15px
type-scale.md dimension 18px
type-scale.lg dimension 24px
type-scale.xl dimension 32px
type-scale.2xl dimension 48px
type-scale.3xl dimension 72px
space.1 dimension 4px
space.2 dimension 8px
space.3 dimension 12px
space.4 dimension 16px
space.5 dimension 24px
space.6 dimension 32px
space.7 dimension 48px
space.8 dimension 64px
space.9 dimension 96px
rule.1 dimension 1px
rule.2 dimension 2px
rule.3 dimension 4px
rule.hairline dimension 0.5px
target.min dimension 24px WCAG 2.5.8 pointer minimum
target.touch dimension 44px comfortable touch
font-family.display fontFamily Archivo free, Figma-native, Helvetica Neue substitute
font-family.mono fontFamily Spline Sans Mono
font-family.serif fontFamily Source Serif 4
font-family.cjk fontFamily Noto Sans JP

semantic-paper

TokenTypeValueNotes
surface.page color {paper.01}
surface.raised color {paper.00}
surface.sunken color {paper.02}
surface.inset color {paper.03}
surface.inverse color {ink.90}
text.primary color {ink.90}
text.body color {ink.80}
text.secondary color {ink.60}
text.muted color {ink.50}
text.disabled color {ink.40}
text.on-fill color {paper.00}
text.on-marigold color {ink.90}
border.strong color {ink.80}
border.default color {ink.40}
border.hairline color {ink.20}
accent.success color {pigment.celadon-ink}
accent.success-fill color {pigment.celadon}
accent.danger color {pigment.cinnabar-ink}
accent.danger-fill color {pigment.cinnabar}
accent.feature color {pigment.aubergine-ink}
accent.feature-fill color {pigment.aubergine}
accent.warning-fill color {pigment.marigold}
accent.info-fill color {pigment.indigo}
field.bg color {paper.00}
field.border color {ink.80}
field.placeholder color {ink.50}
focus.ring color {ink.100}
focus.width dimension {rule.2}
focus.offset dimension 2px

semantic-ink

TokenTypeValueNotes
surface.page color {ink.90}
surface.raised color {ink.80}
surface.sunken color {ink.100}
surface.inset color #26221A
surface.inverse color {paper.01}
text.primary color {paper.00}
text.body color {paper.01}
text.secondary color {paper.04}
text.muted color {ink.20}
text.disabled color {ink.40}
text.on-fill color {paper.00}
text.on-marigold color {ink.100}
border.strong color {paper.04}
border.default color {ink.40}
border.hairline color {ink.60}
accent.success color {dark-accent.success}
accent.success-fill color {pigment.celadon}
accent.danger color {dark-accent.danger}
accent.danger-fill color {pigment.cinnabar}
accent.feature color {dark-accent.feature}
accent.feature-fill color {pigment.aubergine}
accent.warning-fill color {pigment.marigold}
accent.info-fill color {pigment.indigo}
field.bg color {ink.100}
field.border color {paper.04}
field.placeholder color {ink.20}
focus.ring color {paper.00}
focus.width dimension {rule.2}
focus.offset dimension 2px

stock-bone

TokenTypeValueNotes
surface.page color #EFEEE7
surface.raised color #F7F6F1
surface.sunken color #E4E3DB
surface.inset color #D8D7CE
surface.inverse color {ink.90}
text.primary color {ink.90}
text.body color {ink.80}
text.secondary color {ink.60}
text.muted color {ink.50}
text.disabled color {ink.40}
border.strong color {ink.80}
border.default color {ink.40}
border.hairline color {ink.20}
field.bg color #F7F6F1
field.border color {ink.80}
field.placeholder color {ink.50}
focus.ring color {ink.100}

stock-indigo

TokenTypeValueNotes
surface.page color #1B2230
surface.raised color #232C3D
surface.sunken color #141A25
surface.inset color #2B3547
surface.inverse color #EFEEE7
text.primary color #EDECE3 14.0:1
text.body color #E2E1D6 12.2:1
text.secondary color #AEB4C2 7.6:1
text.muted color #9BA3B4 5.2:1, AA
text.disabled color #5E6675
border.strong color #AEB4C2
border.default color #4A5365
border.hairline color #343E4F
accent.success color {dark-accent.success}
accent.danger color {dark-accent.danger}
accent.feature color {dark-accent.feature}
field.bg color #141A25
field.border color #4A5365
field.placeholder color #9BA3B4
focus.ring color #EDECE3