Pulliv0.1
8px
09 — Accessibility

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

PairForegroundGroundRatioRequiredResult
Body text--color-text--color-bg17.994.5Pass AA
Body on panel--color-text--color-surface18.604.5Pass AA
Secondary--color-text-secondary--color-bg7.744.5Pass AA
Captions--color-text-tertiary--color-bg5.584.5Pass AA
Captions on sunken--color-text-tertiary--color-surface-sunken5.044.5Pass AA
Accent link--color-accent--color-bg5.924.5Pass AA
On accent--color-on-accent--color-accent5.924.5Pass AA
Control boundary--color-border-strong--color-bg3.543.0 non-textPass AA
Boundary on panel--color-border-strong--color-surface3.663.0 non-textPass AA
Boundary on sunken--color-border-strong--color-surface-sunken3.203.0 non-textPass AA
Focus ring--color-focus--color-bg5.923.0 non-textPass AA
Hairline (decorative)--color-border--color-bg1.32—Exempt — decorative

Dark theme

PairForegroundGroundRatioRequiredResult
Body text--color-text--color-bg16.254.5Pass AA
Body on panel--color-text--color-surface15.014.5Pass AA
Secondary--color-text-secondary--color-bg8.894.5Pass AA
Captions--color-text-tertiary--color-bg5.534.5Pass AA
Captions on sunken--color-text-tertiary--color-surface-sunken4.514.5Pass AA
Accent link--color-accent--color-bg7.774.5Pass AA
On accent--color-on-accent--color-accent7.774.5Pass AA
Control boundary--color-border-strong--color-bg5.083.0 non-textPass AA
Boundary on panel--color-border-strong--color-surface4.693.0 non-textPass AA
Boundary on sunken--color-border-strong--color-surface-sunken4.143.0 non-textPass AA
Focus ring--color-focus--color-bg7.773.0 non-textPass AA
Hairline (decorative)--color-border--color-bg1.41—Exempt — decorative
Tertiary text has the smallest margin in the system — 5.04 in light and 4.51 in dark against --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.
Every earlier version of this table tested against --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.
Both border tokens are not equal. --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: none without 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 tabindex values above zero.
  • Focus is restored after any overlay closes, to the element that opened it.
try it with the keyboard — ring appears on tab, not on click

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.

TokenP = 6P = 8P = 10Interactive?
--alavu-xs18px24px30pxNo — badges only
--alavu-sm24px32px40pxMeets 2.5.8 (AA, 24px) at every density; the AAA 44px target is supplied by an expander, not by the box
--alavu-md44px floored44px55pxYes
--alavu-lg48px floored56px70pxYes
Visual size and target size are different measurements. A 32px row action may keep its 32px appearance provided its interactive area is padded to 44px. Conflating the two is how dense tables end up unusable on touch, and the system will not let the visual token be read as a target guarantee.
This page asserted the 44px rule for two rounds before the code enforced it. An audit of the stylesheet found .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.

MotionPurposeUnder reduced motion
Hover, press, colourfeedbackInstant. Colour still changes — the information is preserved, the transition is not.
Expand, revealcontinuity of placeInstant. Content appears in its final position.
Toggle loop travelstate changeInstant — the loop jumps to the other dot. Position and colour still communicate the state.
Page transitionspatial relationshipRemoved entirely. Cross-fade is not a substitute; it triggers the same vestibular response.
Kolam draw-ondecorationRemoved 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-1 on a busy ground they will not clear it — raise the weight or lower the density.
  • Always aria-hidden when 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.

How to measure density. Screenshot at the smallest supported viewport, threshold the image, and compute the ratio of pattern pixels to total. Over 10% is a change request, not a discussion. The check is cheap enough to run in CI on key screens.

Semantics per component

What the library styles versus what your markup must declare. The CSS reacts to state; it does not set it.

ComponentYou must provideLibrary handles
Buttonaccessible name, aria-busy when loading, disabledfocus ring, states, target size
Fieldlabel, aria-invalid, aria-describedby, requirederror styling, focus glow, height
Choicelabel wrapping, name grouping for radiostarget size, focus, switch travel
Badgea text label — alwayscolour, shape
Alertrole="alert" or role="status", or neitheraccent bar, colour, layout
Tabsrole, aria-controls, aria-labelledby, idsarrow-key navigation, roving tabindex, panel visibility
Tableth scope, aria-sort, captionrules, hover, header ground
Progressrole, aria-valuenow/min/max, text statustrack and fill
Empty statearia-hidden on the illustrationlayout, art sizing
Toastrole="status", timing, queueingelevation, layout
Modalrole="dialog", aria-modal, focus trap and restore, Escape, inert backgroundlayout, elevation, sizing
This split is deliberate. A library that sets ARIA state for you is a library that gets it wrong in the cases it did not anticipate, silently. Styling reacts to correct markup; it cannot manufacture it.

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: none without 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.