Buttons
Printed keys. 36px minimum target, square caps, optional radius via --lk-radius.
After the Asian manuscript page
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.
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.
Warm paper surfaces, warm-tinted ink text, saturated pigment grounds. Marigold is the hero.
Paper, Ink, Bone, Indigo. Every text role clears AA on each.
Three tiers (primitives, semantic, stocks) built with Style Dictionary to CSS, SCSS, and JS.
Three ways in. Set the stock with data-theme on <html> (default is paper).
npm install @lokta/tokens @lokta/css
@import "@lokta/tokens/css/lokta.css"; @import "@lokta/css/lokta.css";
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.
<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.
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.
| Dial | Range (the guardrail) |
|---|---|
| Stock | a curated set (paper, manuscript, bone, ink, indigo, highland, slate, steel, onyx, and light variants), not a freeform background |
| Accent | a curated pigment (marigold, madder, lac, cinnabar, indigo, …), not a colour picker |
| Voice | named typeface options per role (Archivo or Mukta for display, Source Serif or Martel for serif). Script is automatic by language, never a dial |
| Density | comfortable or compact. Two steps |
| Radius | clamped 0 to 3px. Square is the default and the ceiling is gentle |
| Grain | off, subtle, or fibrous. Texture, never depth |
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.
| System | Consumer dials |
|---|---|
| Radix Themes | accent, gray, appearance, radius, scaling, panel (about 6) |
| Material 3 | a seed colour plus light/dark (about 1 to 2) |
| USWDS | colour families, spacing base, type scale, font families (a handful) |
| Lokta | Stock, 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.
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.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.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.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
Headline primary
Body text on the page surface, AA across every role.
Secondary · muted
Headline primary
Body text on the page surface, AA across every role.
Secondary · muted
Headline primary
Body text on the page surface, AA across every role.
Secondary · muted
Headline primary
Body text on the page surface, AA across every role.
Secondary · muted
A real type set: every size pairs a line-height, weight, and tracking.
| Role | Token | Size / LH | Weight | Tracking | Use |
|---|---|---|---|---|---|
| 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 |
Archivo. A neutral editorial grotesk.
Spline Sans Mono 0123
Source Serif 4 pull quote
光の写本
রান্না খাদ্য পুষ্টি
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
Twelve columns, 24px gutters (space-5), on an 8px base. Body measure caps near 72ch for readability. Breakpoints: 480 · 768 · 1024 · 1440.
Paper does not bounce. Productive easing for feedback, expressive for entrances. Honors prefers-reduced-motion.
| Token | Value | Use |
|---|---|---|
--ease-paper | cubic-bezier(0.2, 0, 0.1, 1) | UI feedback |
--ease-productive | cubic-bezier(0.2, 0, 0.38, 0.9) | State changes |
--ease-expressive | cubic-bezier(0.4, 0.14, 0.3, 1) | Entrances |
--dur-fast | 120ms | Hover, press |
--dur-base | 200ms | Most transitions |
--dur-slow | 320ms | Overlays |
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.
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.
| Element | Rule | From the book |
|---|---|---|
| Titles and headings | Title Case, minor words lower. Spell out "and", never "&". Oxford comma. | Soups, Stews, and Noodles |
| Labels and metadata | Sentence case with a colon; units spelled out. | Preparation time: 3 minutes |
| Quantities | Real fractions, imperial first with metric in parentheses, tabular figures (Datatype and .lk-frac). | 2 1/2 cups (600 ml) water |
| Instructions and buttons | Imperative, verb first; calm and precise, with reassuring asides. | Stir with a wet wooden spatula. |
| Asides and tips | Short footnotes, practical and warm. Em dashes are welcome. | * Set a timer—it is practical. |
| Tone | Never sell, never hype. The gate bans the marketing words. | The book never markets a recipe. |
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.
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.
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.
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.
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.
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.
| 2 1/2 | cups (600 ml) water |
| 3/4 | tsp fine salt |
| 1 1/3 | cups bread flour |
| 45 | g 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.
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.
Printed keys. 36px minimum target, square caps, optional radius via --lk-radius.
Hard-cornered metadata pills.
Text, select, textarea, with placeholder and disabled states.
Square caps; the radio reads with an inner filled square.
Live: click or use Left/Right, Home/End. Roving tabindex, real ARIA.
Live: Enter or Space toggles each panel (aria-expanded + region).
Live: ArrowDown opens, arrows move, Escape closes and restores focus.
Live: opens with focus trap, Escape closes, focus returns to the trigger.
Choose a paper for the run. The choice re-points every semantic token.
Color is paired with a glyph, so meaning never relies on hue.
Hover or focus the button.
npm install @lokta/tokens @lokta/css
Inline --surface-page too.
Tracked mono headers, hairline rules, striped rows, tabular figures.
| Stock | Surface | Roles |
|---|---|---|
| Paper | paper-01 | 12 |
| Ink | ink-90 | 12 |
| Indigo | #1B2230 | 12 |
The one shadow in the system: a single hard offset, no blur.
Choose a paper for the run. The choice re-points every semantic token.
Rules, the measured rule, and the hatched end-mark.
Running head, colophon, folio.
Whole pages built only from Lokta classes, each part of the axe-core suite. Open one, or copy its markup as a starting point.
A film-dish browser: live search, tag filters, a recipe dialog, save-to-list, and a stock switcher.
App shell, KPI stat cards, a data table, segmented control, toast, and an empty state.
The marketing kit: an on-pigment hero, features, pricing, a testimonial, and a CTA band.
Every application-tier component on the drop-in, with a live stock switcher.
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.
| Class | Pigment | Use |
|---|---|---|
hero | marigold | the one node to read first |
store | celadon | a datastore |
dec | indigo | a decision |
danger | cinnabar | a failure or drop |
muted | paper | secondary |
// 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.
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.
| Template | What it is |
|---|---|
lokta-tech | White technical report |
lokta-report | Cream editorial report |
lokta-article | Long-form editorial |
lokta-bulletin | Single-sheet notice |
lokta-letter | Correspondence |
lokta-cover | Pigment ground with the vertical spine |
lokta-recipe | After the cookbook page |
#import "@local/lokta:0.1.0": * #show: lokta-recipe.with(title: "Dashi Broth", film: "Spirited Away", ..)
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/.
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| Token | Type | Value | Notes |
|---|---|---|---|
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| Token | Type | Value | Notes |
|---|---|---|---|
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| Token | Type | Value | Notes |
|---|---|---|---|
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| Token | Type | Value | Notes |
|---|---|---|---|
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| Token | Type | Value | Notes |
|---|---|---|---|
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 |