Where the tradition has to yield.
Kolam is high-frequency line work in a high-contrast palette. Some of that helps users and some of it fights them. This page names both, publishes measured numbers rather than intentions, and sets the limits that the rest of the system is not allowed to argue with.
Contrast matrix
Every figure below is computed from the palette using the WCAG 2.x relative-luminance formula, not estimated. The same computation runs in CI; a claim that drifts from its measurement fails the build.
Light theme
| Pair | Foreground | Ground | Ratio | Required | Result |
|---|---|---|---|---|---|
| Body text | --color-text | --color-bg | 17.99 | 4.5 | Pass AA |
| Body on panel | --color-text | --color-surface | 18.60 | 4.5 | Pass AA |
| Secondary | --color-text-secondary | --color-bg | 7.74 | 4.5 | Pass AA |
| Captions | --color-text-tertiary | --color-bg | 5.58 | 4.5 | Pass AA |
| Captions on sunken | --color-text-tertiary | --color-surface-sunken | 5.04 | 4.5 | Pass AA |
| Accent link | --color-accent | --color-bg | 5.92 | 4.5 | Pass AA |
| On accent | --color-on-accent | --color-accent | 5.92 | 4.5 | Pass AA |
| Control boundary | --color-border-strong | --color-bg | 3.54 | 3.0 non-text | Pass AA |
| Boundary on panel | --color-border-strong | --color-surface | 3.66 | 3.0 non-text | Pass AA |
| Boundary on sunken | --color-border-strong | --color-surface-sunken | 3.20 | 3.0 non-text | Pass AA |
| Focus ring | --color-focus | --color-bg | 5.92 | 3.0 non-text | Pass AA |
| Hairline (decorative) | --color-border | --color-bg | 1.32 | — | Exempt — decorative |
Dark theme
| Pair | Foreground | Ground | Ratio | Required | Result |
|---|---|---|---|---|---|
| Body text | --color-text | --color-bg | 16.25 | 4.5 | Pass AA |
| Body on panel | --color-text | --color-surface | 15.01 | 4.5 | Pass AA |
| Secondary | --color-text-secondary | --color-bg | 8.89 | 4.5 | Pass AA |
| Captions | --color-text-tertiary | --color-bg | 5.53 | 4.5 | Pass AA |
| Captions on sunken | --color-text-tertiary | --color-surface-sunken | 4.51 | 4.5 | Pass AA |
| Accent link | --color-accent | --color-bg | 7.77 | 4.5 | Pass AA |
| On accent | --color-on-accent | --color-accent | 7.77 | 4.5 | Pass AA |
| Control boundary | --color-border-strong | --color-bg | 5.08 | 3.0 non-text | Pass AA |
| Boundary on panel | --color-border-strong | --color-surface | 4.69 | 3.0 non-text | Pass AA |
| Boundary on sunken | --color-border-strong | --color-surface-sunken | 4.14 | 3.0 non-text | Pass AA |
| Focus ring | --color-focus | --color-bg | 7.77 | 3.0 non-text | Pass AA |
| Hairline (decorative) | --color-border | --color-bg | 1.41 | — | Exempt — decorative |
--color-surface-sunken, its worst background. It clears AA and very little more. It is for captions, metadata and hints only. Nothing a user must read to complete a task may be set in --color-text-tertiary, and it must never be combined with a reduced font weight.--color-bg only, and that is where four failures hid. Components do not sit on the page background — they sit on --color-surface and --color-surface-sunken. Measured against those, --color-text-tertiary came to 4.46 in light and 4.14 in dark against the 4.5 requirement, and --color-border-strong — the pulli, the kambi, the colour the entire state language is drawn in — came to 2.94 in light and 2.63 in dark against the 3.0 requirement. The worst case was the sort indicator on a table header, because table headers use the sunken surface: the one mark the system offers as proof that state is shape rather than colour was the mark that failed to meet contrast on the surface it actually renders on. Four roles were repointed one rung along the existing ramp, which fixed all eight failures without touching a palette value. A contrast table is only as good as the backgrounds it thinks to test.--color-border is decorative — dividers, card edges — and is not required to clear 3:1. --color-border-strong is the boundary of an interactive control and is. Using the decorative one on an input is the single most common accessibility regression in a token system, so the names were chosen to make it awkward.Focus
One ring, everywhere, never removed.
- Width--border-edge · 2px
- Offset--border-edge · 2px
- Colour--color-focus
- Contrast vs ground5.92 / 7.77
- Stylesolid outline
The offset matters as much as the width: a ring drawn flush against an accent-filled button disappears into it. Two pixels of ground between the control and the ring keeps it legible on every variant without needing a per-variant ring colour.
Rules
:focus-visible, not:focus. Mouse users do not get a ring on click; keyboard users always do.- Never
outline: nonewithout an equivalent replacement in the same declaration. - Focus is never colour alone. A border colour change is not a focus indicator — it fails for anyone who cannot see the difference.
- Focus order follows reading order. If it does not, the DOM is wrong; do not repair it with
tabindexvalues above zero. - Focus is restored after any overlay closes, to the element that opened it.
Target size
The one place the system overrides its own derivation, and it does so deliberately.
Control heights are derived from the pitch: --alavu-md is 5.5 P. At the default pitch of 8 that is 44px exactly. At compact density, P is 6, and the derivation gives 33px — below the WCAG 2.5.8 minimum.
The token therefore reads max(calc(var(--p) * 5.5), 44px). Arithmetic loses to accessibility, in the token itself rather than in a patch applied per component. It is listed in where derivation breaks and it is the model for how any future exception must be handled.
| Token | P = 6 | P = 8 | P = 10 | Interactive? |
|---|---|---|---|---|
| --alavu-xs | 18px | 24px | 30px | No — badges only |
| --alavu-sm | 24px | 32px | 40px | Meets 2.5.8 (AA, 24px) at every density; the AAA 44px target is supplied by an expander, not by the box |
| --alavu-md | 44px floored | 44px | 55px | Yes |
| --alavu-lg | 48px floored | 56px | 70px | Yes |
.btn-sm and .btn-icon.btn-sm resolving to 32px at the default pitch and 24px at compact, with no hit-area expansion anywhere in the CSS — the requirement was written down, marked as a condition of use, and never implemented. It is implemented now, as a ::before that expands the pointer target to --alavu-md without changing a pixel of the visual box, and it is opted out of every component whose ::before already carries a kolam mark. The claim has also been narrowed to what is actually true: small controls clear WCAG 2.5.8 at AA on their own geometry, and reach the AAA 44px figure through the expander. A documented requirement that nothing checks is a requirement that is not met.Motion
Reduced motion is not slower motion. It is the removal of motion that carries no information. Summarised here; the full policy, the choreography ceiling and the testing checks are in 08 · Motion.
| Motion | Purpose | Under reduced motion |
|---|---|---|
| Hover, press, colour | feedback | Instant. Colour still changes — the information is preserved, the transition is not. |
| Expand, reveal | continuity of place | Instant. Content appears in its final position. |
| Toggle loop travel | state change | Instant — the loop jumps to the other dot. Position and colour still communicate the state. |
| Page transition | spatial relationship | Removed entirely. Cross-fade is not a substitute; it triggers the same vestibular response. |
| Kolam draw-on | decoration | Removed entirely. The static kolam renders immediately. Any accompanying status stays as text. |
Implementation
The stylesheet collapses all durations under the media query, and the generator checks the same query in JavaScript before running its draw-on. Two mechanisms because the animation is not purely declarative — and a CSS-only guard would leave the JS path running.
@media (prefers-reduced-motion: reduce) {
* { transition-duration: .001ms !important;
animation-duration: .001ms !important; }
}
Limits
- Nothing loops indefinitely except an explicit loading indicator, which stops when loading stops.
- No parallax, no auto-play, no motion tied to scroll position.
- Nothing flashes more than three times per second — the draw-on staggers segments but never blinks.
- Duration ceiling of 600ms for anything a user is waiting on.
The pattern layer
The expressive part of this system is also the part most able to harm it. These limits are hard.
Hard limits
- Density ≤ 10% of any viewport at any breakpoint. Kolam belongs to empty states, onboarding, brand moments and print — never behind body text, never in table chrome, never as a persistent page ground at full strength.
- Never the sole carrier of meaning. A motif may reinforce a state; it may not encode one. Every state needs text plus a non-pattern cue.
- 3:1 for line art. Kolam strokes are non-text graphics. At
--kambi-1on a busy ground they will not clear it — raise the weight or lower the density. - Always
aria-hiddenwhen decorative, which is nearly always. A generated pattern has no informational content to announce. - Never behind text without a solid surface between them. Not at reduced opacity, not blurred.
A page ground at 12% opacity, behind nothing. This is the maximum acceptable treatment for a full-bleed pattern — and even here, any text placed over it must sit on its own opaque surface.
Semantics per component
What the library styles versus what your markup must declare. The CSS reacts to state; it does not set it.
| Component | You must provide | Library handles |
|---|---|---|
| Button | accessible name, aria-busy when loading, disabled | focus ring, states, target size |
| Field | label, aria-invalid, aria-describedby, required | error styling, focus glow, height |
| Choice | label wrapping, name grouping for radios | target size, focus, switch travel |
| Badge | a text label — always | colour, shape |
| Alert | role="alert" or role="status", or neither | accent bar, colour, layout |
| Tabs | role, aria-controls, aria-labelledby, ids | arrow-key navigation, roving tabindex, panel visibility |
| Table | th scope, aria-sort, caption | rules, hover, header ground |
| Progress | role, aria-valuenow/min/max, text status | track and fill |
| Empty state | aria-hidden on the illustration | layout, art sizing |
| Toast | role="status", timing, queueing | elevation, layout |
| Modal | role="dialog", aria-modal, focus trap and restore, Escape, inert background | layout, elevation, sizing |
Cultural handling
Accessibility includes not doing harm to the people whose practice this is built on.
What this system requires
- Name the source. Tamil terminology is used with glosses throughout, never as exotic labelling. The README credits kolam practice and the formal-grammar research — Gift Siromoney and colleagues at Madras Christian College, from the 1970s — that established kolams as a generable picture language and made this engine possible. The rationale for choosing this tradition at all, its contested chronology, and the threshold traditions elsewhere that arrived at the same constraints are set out in 01 · Origins.
- Avoid the sacred set. Some kolams are tied to specific festivals, to mourning, and to temple thresholds. The generator produces secular looped patterns only, and a denylist of ritual configurations ships alongside it.
- Assess fit before adoption. This language carries meaning. For a product with no relationship to that context it risks reading as ornament borrowed for texture. Say so in your own README rather than letting your users be the ones who point it out.
- Route credit and, where there is revenue, resources back toward the practice and the communities that sustain it. Kolam is authored by generations of women and is largely uncredited; an open-source system built on it should not repeat that.
What this system does not claim
- Not authoritative. This is one reading of the form by a design system, not a documentation of the tradition. Regional practice varies enormously and this engine captures a narrow slice.
- Not complete. The tile decomposition covers square kolams. Diamond grids, padi kolams, freehand rangoli and coloured-powder forms are outside it.
- Not a substitute for consultation. If you are shipping this to an audience for whom kolam is a living daily practice, ask them. The engine will not tell you what is appropriate on a given morning.
Testing checklist
Split by what a machine can settle and what it cannot.
Automated — runs in CI
- Recompute every published contrast figure from the palette; fail on drift.
- axe-core on every documented component in both themes and all three densities.
- Assert no
outline: nonewithout a replacement in the same rule. - Assert every interactive element resolves to ≥ 24px of its own geometry at P = 6, and ≥ 44px once the target expander is counted. Assert the expander is present on every control below
--alavu-md. - Assert generated kolams have zero open endpoints.
- Lint for length literals outside
tokens.css.
Manual — every release
- Keyboard-only pass through every flow: reachable, visible, escapable, focus restored.
- Screen reader pass on forms, tabs and modals — VoiceOver and NVDA at minimum.
- 200% zoom and 400% reflow with no horizontal page scroll.
- Reduced-motion pass: confirm removal, not shortening.
- Greyscale pass: does every status still read?
- Pattern density screenshot at the smallest viewport.