Skip to content
Ahimsa

Patterns

Compositions, and the reasoning inside them

A primitive tells you what a button looks like. A pattern tells you what to do when someone is about to lose something.

The core pattern

An equal-weight choice

Wherever this system offers a way out, that way out is a real control with the same metrics as the way forward. This is the single most repeated decision in the whole system, and it is the one most often got wrong.

Do this

An outlined pill beside a filled one. Both are buttons, both are the same size, and declining is a legitimate answer.

Not this

no, I don't want to keep my data safe

Gray text reads as a caption rather than a choice, and confirmshaming puts a cost on saying no. Both are refused.

<div class="ah-actions">
  <button class="ah-btn" type="button">Delete it</button>
  <button class="ah-btn ah-btn--ghost" type="button">Keep it</button>
</div>

Label the escape hatch for what it is, not for what it is not: "Keep it", "Not now", "Leave it open", "Stay here". Never "Cancel" where a warmer word is true, and never a sentence engineered to make you feel small.

The pull model

Checking for a new version

Most systems ship a bar that slides in to tell you an update is ready. Ahimsa does not, and this is the replacement. Version news is something you look up, never something you are told — whatever you were doing is what you should still be doing.

Do this — in Settings

Version

You are running 1.4.0.

The person presses. The answer lands inline, beside the control that asked for it.

Not this

A newer version is ready. Update

A bar that slides in over your work. It is dismissible, and it is still an interruption: it arrived on its own schedule and spent your attention to do it.

The same reasoning rules out modals on load, cookie banners, rating prompts, push notifications, beforeunload nags and autoplay. tools/check-attention.mjs fails the build on all of them.

When something breaks

Error without alarm

There is no red in this system, and no error banner. If something did not work, the app says so in the same voice it says everything else — and it says what is still true, because the thing a person most needs to know is whether they have lost anything.

Do this

That did not go through

The file could not be read. Nothing was changed, and your cards are as they were.

Not this

Error: IMPORT_FAILED (0x2F) — invalid input. Please try again.

A code, a scold, and no answer to the only question that matters. "Invalid input" also puts the fault on the person.

And failures are never silent. A control that goes quiet and stays dead leaves someone tapping at a screen with no idea whether they did something wrong. Say what happened.

Layout

Two shells, and picking the right one

.ah-page is a normal document: fluid gutters, a reading measure, scrolling the way the browser intends. .ah-app is a fixed full-viewport application frame with safe-area insets, widening in steps so a desktop window gets a comfortable app rather than a narrow strip.

The trade is real: a fixed shell cannot reflow the way a document can, so do not reach for it to hold an article. This site uses .ah-page.

<!-- a document -->
<div class="ah-page">
  <article class="ah-measure ah-stack">…</article>
</div>

<!-- an installed app -->
<div class="ah-app">
  <section class="ah-screen">
    <header class="ah-hdr">…</header>
    <div class="ah-scroll">…</div>
    <footer class="ah-footer">…</footer>
  </section>
</div>

The app shell needs <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">. Note what is absent from that: no maximum-scale, no user-scalable=no. Pinch zoom always stays.

Two details worth keeping

The scroll tail is a block, not padding. End padding on a flex scroll container is not reliably part of what the browser will scroll to. A block is content, and every engine scrolls to the end of its content.

overscroll-behavior-y, not both axes. Setting it on x swallows horizontal trackpad swipes, which can turn a pager into a one-way trap with no pointer route back.

Surfaces

Putting text on a gradient

Use .ah-surface--<name> rather than setting a gradient by hand. The class carries the gradient and the ink measured against its worst stop, so anything inside reads correctly without you checking.

carries text

The surface brings its own ink.

Heading, body and muted are all measured against this ramp's darkest stop.

surface only

Five of Day's gradients span too wide a lightness range for any ink to clear AA across them. They are backdrops — text on them goes in an opaque card, exactly like this one.