Made to be erased.
The fourth principle is a release policy, not a poem. A kolam lasts a day and is redrawn the next morning; the value is in the drawing, not the artefact. Systems that cannot remove things die of accretion, so this one schedules its own sweeping.
Versioning
Semantic versioning, with one breaking release a year and a name for it.
| Change | Bump | Examples |
|---|---|---|
| Major | 1.0.0 → 2.0.0 | Removing a token or component; renaming a semantic role; changing the seed relationship; altering a default that shifts layout. |
| Minor | 1.0.0 → 1.1.0 | Adding a token, component or variant; deprecating something (with the warning, not the removal); new generator options with safe defaults. |
| Patch | 1.0.0 → 1.0.1 | Bug fixes; contrast corrections that keep the same role; documentation; test coverage. |
What counts as breaking
- Any semantic token rename or removal. Semantics are the public API.
- A primitive change that alters resolved values at the default pitch.
- A component's default appearance changing enough to shift layout — a height, a padding, a radius step.
- Generator output changing for a given seed. Determinism is a promise; if a fix must change output, it is a major version and the release notes say which seeds moved.
Not breaking: adding a token, adding a variant, tightening a contrast value within the same role, or any change confined to the palette layer that preserves every published contrast figure.
Contribution
One unusual rule, and it is the one that keeps the system coherent.
P and r. A raw length literal is a review block, not a nit. If the value genuinely cannot be derived, the PR must say which external constraint forces it, land it in tokens.css with an inline comment, and add a row to the table in Lattice → where derivation breaks.Pull request template
## What One sentence. ## Derivation | value | expression | at P=8 | |-------|-----------|--------| | height | 5.5 P | 44px | | radius | 2 r | 8px | ## Layer [ ] palette [ ] primitive [ ] semantic [ ] component References only the layer above it: yes / no ## Accessibility Contrast measured: … Target size at P=6: … Reduced motion: … ## Sweep plan What replaces this, and when, if it does not last.
Two layers only
Primitives and semantic aliases. There is no third layer of component-scoped tokens — no --button-padding-x. A component that needs its own token is a component whose spec has drifted from the system, and the correct fix is to bring it back rather than to give it a private vocabulary.
The generator is the source of truth
No hand-drawn SVG pattern assets enter the repository. They cannot be re-derived when the pitch or the palette changes, which means they cannot be swept and redrawn — and a pattern that cannot be redrawn is exactly what principle 04 exists to prevent.
Review criteria
What a reviewer checks, in order. The first failure ends the review — no point discussing colour if the layer direction is wrong.
| # | Check | Fails if |
|---|---|---|
| 1 | Layer direction | The change references a layer below itself, or a component references a palette or primitive value. |
| 2 | Derivation | A length literal appears outside tokens.css without a documented exception. |
| 3 | Accessibility floors | Contrast under 4.5 for text or 3.0 for non-text; target under 44px at P = 6; focus indicator removed or colour-only. |
| 4 | Both themes | The change was only checked in one theme. |
| 5 | All three densities | Layout breaks at P = 6 or P = 10. |
| 6 | Principle conflict | The change violates a principle without invoking the precedence table in Principles → when they conflict. |
| 7 | Emphasis budget | The change adds a second emphasised element to a view without removing one. |
| 8 | Sweep plan | A component is added with no account of what would replace it. |
Deprecation
Sweeping is scheduled, announced, and tooled. It is never a surprise and never indefinite.
The lifecycle
Marked. Deprecation notice in the docs, a console warning in development builds, and a codemod published the same day. The replacement must already exist.
Last call. The warning escalates and names the version that will remove it. Docs move the component to a "sweeping" section.
Swept. Removed from the codebase entirely. No legacy directory, no compatibility shim, no flag to bring it back.
In the ledger. The entry stays in the removal ledger forever, with the date, the reasoning and the replacement. The record of what was swept is part of the system.
The removal ledger
A single page listing everything ever removed. It is the cheapest documentation in the system and the most useful in two years, when someone asks why there is no accordion.
## Swept ### `.btn-tertiary` — removed 2.0.0 Three button variants were carrying two levels of priority. Ghost absorbed the use cases. → replacement: `.btn-ghost` → codemod: `npx pulli-codemod btn-tertiary` ### `--color-text-muted` — removed 2.0.0 Duplicated `--color-text-tertiary` at a different contrast, and shipped below 4.5 twice. → replacement: `--color-text-tertiary`
Adoption tiers
Three ways to use this, each honest about what it gives you and what it costs.
Tier 1 · Pattern only
Take: kolam.js.
Get: generated marks, empty states and grounds inside your existing system.
Cost: 4 KB. You still owe the density limit and the cultural handling.
Tier 2 · Tokens
Take: tokens.css, map your components onto the semantic layer.
Get: derivation discipline, both themes, three densities, the measured contrast matrix.
Cost: a migration of every hardcoded value you own. This is the real work and it is worth doing once.
Tier 3 · Full system
Take: everything.
Get: twelve components with accessibility contracts, and the principles as a shared review vocabulary.
Cost: twelve components is a foundation, not a product library. You will build the other thirty and should expect to contribute them back.
The verification harness
What is checked automatically, what it found the first time it was run in full, and why several of those things had been asserted on other pages for weeks before anything checked them.
A design system that publishes numbers has to be able to prove them. Five harnesses run over the whole system: the generator's geometry, the token graph, every contrast pair, the documented usage limits, and the link and document structure of all twenty-one pages. They are the reason the figures elsewhere in this documentation can be disagreed with rather than merely believed.
What each harness asserts
| Harness | Assertions | Result |
|---|---|---|
| Geometry | Every arc has radius r = U/2 and its centre on a lattice dot; no open endpoints; deterministic output per seed; dot counts normalise to odd; cells = (n−1)²; boundary loops match the published formula; the draw-on never exceeds its cap; r = unit/2 for both frame styles at every unit. | 480 configurations · 71,354 arcs · 0 failures |
| Token graph | Every var() resolves; the palette is not referenced outside the token layer; no raw hex in product CSS; type does not scale with pitch; every density tier resolves; no dead tokens. | 141 defined · 131 consumed · 0 unresolved · 0 leaks |
| Contrast | Every text and non-text pair in five themes — light, dark by media query, dark by attribute, AAA high-contrast, and print — against bg, surface and surface-sunken. | 0 failures |
| Limits | No CSS duration exceeds the motion cap; the cap and segment duration have exactly one source of truth; the inventory's group counts sum to the published denominator. | 0 breaches · 46 + 4 + 8 = 58 |
| Structure | All internal links and anchors resolve; the sidebar is byte-identical on every page; exactly one aria-current="page" per document, pointing at itself; one h1 per page; no skipped heading levels. | 813 links · 21/21 pages · 0 failures |
What it found
Eleven defects, on a system whose earlier rounds had each ended with a verification pass. They are listed in full because the pattern in them is more useful than the fixes.
| Defect | Severity | How it survived |
|---|---|---|
--color-border-strong at 2.63:1 on surface-sunken in dark, against a 3.0 requirement. This is the colour the pulli and the kambi are drawn in, and table headers use the sunken surface — so the sort indicator, the system's own example of state carried by shape, failed contrast on the surface it renders on. | Critical | Every previous contrast table tested against --color-bg only. Components do not sit on the page background. |
--color-text-tertiary at 4.46 light / 4.14 dark on surface-sunken, against 4.5. | Critical | Same blind spot. |
The 44px target-size rule was documented as a condition of use, marked in the token table, listed in the audit checklist — and implemented nowhere. .btn-sm resolved to 32px at default pitch and 24px at compact with no hit-area expansion in the stylesheet. | Critical | Writing a requirement down is not the same as enforcing it, and nothing checked. |
--motion-cap was published as the ceiling and consumed by nothing; kolam.js restated 600ms as a literal. Two independent declarations of one rule. | Major | The token existed, so it looked wired up. |
framePath() silently ignored unit for the curl style, reading only radius. A caller setting unit got a frame whose local pitch disagreed with the one they asked for, and the two frame styles could disagree with each other from a single call. | Major | Both defaults happened to give r = 16, so the bug was invisible at default settings. |
Two palette leaks into product CSS — --maa-50 in .btn-danger, --mann-900 in .scrim — breaking the one-way layer dependency the system publishes as a rule. | Major | Two roles were missing from the semantic layer, so the palette was the only place to reach. |
aria-current="page" on five in-page anchors across four pages, giving those documents more than one current item. | Major | Copied from the sidebar pattern, where it was correct. |
Heading levels skipped from h2 to h4 on twenty of twenty-one pages — 110 panel titles. | Major | h4 was chosen for its default size rather than its level, on the first page, and copied everywhere after. |
Two pages carried two page-level h1s. | Minor | A demo heading and a page heading in one document. |
| Six tokens defined and referenced by nothing. | Minor | Left behind by earlier revisions. |
| The curl and scallop frames were seeded from different local pitches — 4P and 6P — from the same call site, with no stated reason. | Minor | Two parameters were passed where one rule applies. |
The pattern
Nine of the eleven are the same failure in different clothing: a rule that was written down and then not connected to anything that could check it. The 44px floor, the motion cap, the layer dependency, the heading hierarchy — each was stated as a principle on one page while the code went its own way on another. The two contrast failures are a variation: the check existed and ran clean, but it tested the wrong backgrounds, so it certified a system that failed on the surfaces components actually sit on.
This is worth being precise about, because the obvious lesson — write more tests — is the wrong one. Every one of these had documentation. What none of them had was a path from the documentation to an assertion. The correction is not more prose about correctness; it is that a claim in the documentation and a check in the harness are now required to ship together, and that is a rule in review criteria rather than an aspiration here.
Decision records
Short, dated, never edited. New decisions supersede old ones rather than overwriting them.
# DR-004 · Elevation has three levels, not six Date: 2026-03-11 Status: accepted (supersedes DR-002) ## Context Six elevation levels were proposed to match the component set. ## Decision Three. Depth is carried primarily by surface stepping (sunken / bg / surface); shadow is reserved for things that genuinely float and can be dismissed. ## Reasoning A kolam is flour on a floor and casts no shadow. More practically, elevation ramps become a second unowned hierarchy that competes with type and colour, and nobody can say what level 4 means versus level 5. ## Consequences Popovers and cards share --elevation-panel. Distinguishing them is the job of position and border, not depth.
| DR | Decision | Status |
|---|---|---|
| DR-001 | r = P/2 is fixed and forks may not change it | accepted |
| DR-002 | Six elevation levels | superseded by DR-004 |
| DR-003 | Type scale uses √2 and does not follow the pitch | accepted |
| DR-004 | Elevation has three levels | accepted |
| DR-005 | Target size floors override derivation, inside the token | accepted |
| DR-006 | Dark is the native theme; light is the inversion | accepted |
| DR-007 | No component-scoped token layer | accepted |
Roadmap
Stated as intentions with reasons, not as dates.
Next
- Figma variable library mapping the three layers, with density and theme as modes.
- Style Dictionary build emitting CSS, JSON, Swift and Kotlin from one source.
- Density audit tooling — the pattern-coverage check as a CI script rather than a screenshot instruction.
- Diamond-grid kolams in the generator. The tile logic holds; the boundary-closure proof needs redoing for a non-square perimeter.
Considered and declined
- A React package. The CSS is framework-agnostic on purpose; a component wrapper would halve its audience and double the maintenance.
- More elevation levels. See DR-004.
- Coloured kolams. Multi-colour powder forms are festival-specific and land squarely in the sacred set the generator avoids.
- An icon set. Out of scope, and there are good ones already. Pulli specifies icon sizing, not icon drawing.
Credit & licence
The code is MIT. Take it, fork it, ship it commercially, no attribution required for the software.
The practice is not licensed by anyone here. Kolam belongs to the households and communities who draw it, and has for far longer than any of this. What the licence cannot require, the documentation asks for: name the source, keep the glosses, avoid the sacred set, and route credit and resources back where you can.
Research credit. The observation that kolams form a generable picture language — that a formal grammar can produce them — comes from Gift Siromoney, Rani Siromoney and Kamala Krithivasan at Madras Christian College in the 1970s. The full account — including Siromoney's own, notably conservative reading of the tradition's age — is in 01 · Origins. This engine is a small applied consequence of that work and would not exist without it.
If you ship this
- Say in your own README that the visual language derives from kolam practice.
- Keep the Tamil terms and their glosses. Renaming them to English erases exactly the thing worth crediting.
- Read Accessibility → cultural handling before deciding this is right for your product.
- If your product has no connection to the tradition, say that out loud in your design review. It may still be the right call — but it should be a call, not a default.