Så fångar Gradr kontrastfel mot WCAG 2.2 AA vid kompileringstid i 100k rader Svelte, så att tillgänglighetsregressioner stoppar bygget i stället för att nå skolorna.

Tillgänglighet är inte valfritt när man bygger mjukvara för skolor. Enligt den europeiska tillgänglighetsakten (European Accessibility Act) måste digitala produkter och tjänster i hela EU uppfylla WCAG 2.2 AA.
Problemet är att WCAG-efterlevnad är notoriskt jobbig att verifiera. Många problem kräver antingen manuell granskning eller långsamma och underhållstunga end-to-end-tester. Kontrastförhållanden är särskilt tråkiga: en extern granskare hittar felen på 5 sekunder, men för de flesta produktteam är de en ständig underhållsbörda.
WCAG 2.2 AA kräver:
Den visuella presentationen av text och bilder av text har ett kontrastförhållande på minst 4,5:1
Det vanligaste tillvägagångssättet: starta appen, öppna varenda sida, kör ett granskningsverktyg, kisa mot resultaten, åtgärda, upprepa. Det är manuellt, det är långsamt och det går sönder så fort någon pushar en ny färgtoken. Man kan skriva automatiserade tester, men då måste man se till att varje sida, komponent och vy faktiskt renderas, och felen fångas oftast i allra sista änden av utvecklingsprocessen.
Gradrs angreppssätt
Vi lutar oss tungt mot statisk kodanalys på Gradr. Statiska kontroller som linting och typkontroll är inte knutna till en specifik domän i appen. I stället bygger de på mönster i koden, vilket gör att de skalar över hela kodbasen med minimalt underhåll. Vi går igenom vår bredare filosofi kring statisk analys i ett framtida inlägg, men kontrastkontroll är ett bra exempel på hur långt angreppssättet går att driva.
Att säkerställa tillräckliga kontrastförhållanden
Första steget är att acceptera kompromissen. Man måste veta vilken färg allt kommer att ha (eller kan ha) vid kompileringstid, så tillstånd som avgörs vid runtime måste gå att analysera statiskt.
Det betyder att sådant här:
<div class="text-white-500 bg-{isMain ? 'gray-300' : 'blue-500'}">
{@render children()}
</div>Måste bli så här:
{#if bgColor === 'gray-300'}
<div class="text-white-500 bg-gray-300">
{@render children()}
</div>
{:else}
<div class="text-white-500 bg-blue-500">
{@render children()}
</div>
{/if}Obs: Man kan bygga ännu mer sofistikerad logik för att tillåta lite mer dynamik. Vår linter känner till exempel till snippets som definieras i samma fil, så att man inte behöver upprepa sig mer än nödvändigt. Med det sagt måste man dra gränsen någonstans, ingenting är gratis här i världen.
Likaså får bakgrunds- eller textfärger inte placeras inuti <script>-taggar:
<script>
const variants = {
active: 'bg-blue-500',
disabled: 'bg-gray-300',
inactive: 'bg-gray-100',
blocked: 'bg-red-500'
};
</script>De här begränsningarna kan verka smärtsamma, men vi tror starkt på att tydlig avsikt och utförlig kod är helt okej, om det innebär stramare och säkrare kod.
Komponentgränser
Den största utmaningen (med god marginal) är att se till att komponenter kan kommunicera sina tillstånd till verktygen för statisk analys. Komponenter inuti andra komponenter, eller transparenta/halvtransparenta komponenter som renderas ovanpå varandra i DOM:en.
Det löser vi genom att alla komponenter tydligt kommunicerar hur de renderas, så att analysverktygen kan räkna ut resultatet.
- Transparenta komponenter måste deklarera vilken eller vilka textfärger de renderar
- Komponenter som tar emot snippets måste deklarera på vilken bakgrund
- Halvtransparenta komponenter måste deklarera vilken eller vilka färger de renderar.
Det här fungerar rekursivt, så en transparent komponent som innehåller en annan transparent komponent skickar färginformationen vidare till nästa konsument.
Varje komponent deklarerar sina "färglager" — uppsättningen kombinationer av bakgrunds- och textfärg som den kan ge upphov till. Här är ett förenklat exempel:
// InfoBox.svelte — color context declaration
//
// Each entry is a layer: a list of bg/text colors that coexist.
// The tooling walks these layers to compute effective contrast
// against whatever background the parent provides.
const layers = [
// Layer 1: Blue and white text rendered directly (no own background)
[{ text: 'white' }, { text: 'blue-500' }],
// Layer 2: White text on a semi-transparent gray overlay
[{ bg: 'gray-300/50' }, { text: 'white' }],
// Layer 3: Dark red text on a semi-transparent red overlay
[{ bg: 'red-300/50' }, { text: 'red-700' }]
];När en föräldrakomponent renderar <InfoBox/> tar verktyget förälderns bakgrundsfärg och lägger varje lager ovanpå den. För lager 1 (ingen egen bakgrund) kontrolleras white och blue-500 direkt mot föräldern. För lager 2 och 3 blandas först den halvtransparenta bakgrunden ihop med förälderns (alpha blending), och sedan kontrolleras texten mot resultatet. Om någon kombination hamnar under 4,5:1 blockerar CI.
Den som använder InfoBox kan sedan avgöra vilka bakgrunder det går att rendera komponenten på. Ett liknande system används för komponenter som tar emot snippets.
Linting, linting och ännu mer linting
För att tillämpa det här i stor skala (och se till att agenterna faktiskt följer det) är allt inslaget i en tät svit av lint-regler.
- no-conditional-color-class: Förbjuder inline-villkor i Tailwind som involverar klasser för bakgrunds- eller textfärg.
- no-color-classes-in-script: Förbjuder färgklasser som definieras i <script>-block, där lintern inte kan spåra dem till sitt sammanhang i DOM:en.
- require-color-context: Kräver att komponenter deklarerar sin färgkontext (via bgColor/textColor-props eller contrast.dev.ts). Upptäcker när klasserna i templaten och kontrastfilen glider isär. Så fort lintern inte kan avgöra bakgrunds- och textfärg slår den här regeln till.
- wcag-color-contrast: Den viktiga (men enklaste!) regeln, som gör själva färgberäkningen. Verifierar att kontrastförhållandet mellan text och bakgrund uppfyller WCAG AA.
Tillsammans bildar de ett tätt system som säkerställer att alla färgkombinationer går att analysera statiskt och uppfyller WCAG 2.2 AA.
Slutsats
Med det här systemet på plats kan vi i praktiken sluta se kontrastförhållanden som ett problem. Och viktigast av allt: vi behöver inte manuellt dubbelkolla varje LLM-agents arbete efter kontrastproblem. Färgkombinationer som inte uppfyller kraven går helt enkelt inte att uttrycka i vår kodbas. Det ligger i linje med vår bredare filosofi: att flytta så mycket som möjligt av ansvaret för korrekthet till kompileringstid, där det är billigare att åtgärda och underhålla.

