High-contrast theme
WCAG 2.2 AAA-grade preset for accessibility procurement. Token block opts a subtree into the high-contrast skin while the rest of the page stays standard. Light + dark.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Themes & Styling). See the SvGrid documentation for the full API.
About this example
A WCAG 2.2 AAA-grade high-contrast preset for the Svelte 5 data grid, for accessibility procurement. Every chrome surface uses one of two ink colours on one background, borders are one pixel solid, the accent is pure red on light and pure yellow on dark for unmistakable focus rings, and selection, hover and zebra tones keep at least a 7:1 contrast ratio against text. The token block opts a wrapper's subtree into the skin while the rest of the page stays standard.
Procurement-grade accessibility theme. Every chrome surface uses one of two ink colours (foreground / muted) on one background; borders are 1 px solid; the accent colour is pure red on light / pure yellow on dark for unmistakable focus rings; selection + hover + zebra rows pick neighbouring tones with at least a 7:1 contrast ratio against text.
Drop the token block onto any wrapper to opt that subtree into the high-contrast skin - the rest of the page stays normal so users can switch on a per-section basis.
Imports, features and API used
Imports: @svgrid/grid, ../shared/seed
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: firstName (First name), lastName (Last name), department (Department), country (Country), age (Age), salary (Salary), status (Status)
Frequently asked questions
How do I apply the high-contrast theme to one section?
Wrap that section in an element carrying the token block. Only grids inside it change; the rest of the page keeps its normal theme, so users can switch per section.
Which contrast ratios does it target?
Text against every background at 7:1 or better, the AAA level, including selected and hovered rows and zebra stripes. Focus rings use a pure accent colour so they read at any size.
Does it change behaviour?
No. It is CSS custom properties only; keyboard navigation, ARIA roles and editing are unchanged.
Related documentation
Related articles
- Conditional Row Styling in SvGrid - Drive row-level background tints, classes, and styles from your data - overdue invoices, failed jobs, VIP records - without touching the DOM directly.
- Why We Bet on Svelte 5 Runes for a High-Performance Data Grid - Fine-grained reactivity with $state, $derived, and $effect makes surgical cell updates the default, not the exception - and that changes how you build a data grid.
Source code (96-high-contrast-theme.svelte)
<script lang="ts">
/**
* 89. High-contrast theme preset
* ------------------------------
* Procurement-grade accessibility theme. Every chrome surface uses
* one of two ink colours (foreground / muted) on one background;
* borders are 1 px solid; the accent colour is pure red on light /
* pure yellow on dark for unmistakable focus rings; selection +
* hover + zebra rows pick neighbouring tones with at least a 7:1
* contrast ratio against text.
*
* Drop the token block onto any wrapper to opt that subtree into
* the high-contrast skin - the rest of the page stays normal so
* users can switch on a per-section basis.
*/
import {
SvGrid,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
type GridColumns,
} from '@svgrid/grid'
import { makePeople, type Person } from '../shared/seed'
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
let rows = $state<Person[]>(makePeople(40))
let mode = $state<'standard' | 'contrast'>('contrast')
const columns: GridColumns<Person> = [
{ field: 'firstName', header: 'First name', editorType: 'text', width: 140 },
{ field: 'lastName', header: 'Last name', editorType: 'text', width: 140 },
{ field: 'department', header: 'Department', editorType: 'text', width: 150 },
{ field: 'country', header: 'Country', editorType: 'text', width: 110 },
{ field: 'age', header: 'Age', editorType: 'number', width: 100 },
{ field: 'salary', header: 'Salary', editorType: 'number', width: 140,
format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } } },
{ field: 'status', header: 'Status', editorType: 'text', width: 120 },
]
</script>
<section class="hc-shell flex flex-col flex-1 min-h-0 gap-3">
<header class="hc-head shrink-0">
<div>
<h2 class="hc-title">Accessibility · High-contrast preset</h2>
<p class="hc-sub">
WCAG 2.2 AAA-grade contrast (text ≥ 7:1, focus ring ≥ 3:1, no colour-only state encoding).
Procurement teams that publish a VPAT can reuse this token block verbatim.
</p>
</div>
<div class="hc-toggle" role="group" aria-label="Theme mode">
<button type="button" class:on={mode === 'standard'} onclick={() => (mode = 'standard')}>Standard</button>
<button type="button" class:on={mode === 'contrast'} onclick={() => (mode = 'contrast')}>High contrast</button>
</div>
</header>
<div
class="flex-1 min-h-0 hc-host"
class:hc-host-contrast={mode === 'contrast'}
aria-label={mode === 'contrast' ? 'Grid in high-contrast mode' : 'Grid in standard mode'}
>
<SvGrid responsive={true}
columnResize
data={rows}
columns={columns}
features={features}
filterMode="menu"
selectionMode="cell"
showRowNumbers={true}
enableInlineEditing={true}
enableCellSelection={true}
rowHeight={36}
containerHeight="100%"
fitColumns={true}
/>
</div>
<details class="hc-tokens shrink-0">
<summary>Show token block</summary>
<pre><code>{`/* Drop onto any wrapper to opt that subtree into high-contrast. */
.high-contrast {
--sg-bg: #ffffff;
--sg-fg: #000000;
--sg-muted: #1a1a1a;
--sg-border: #000000;
--sg-header-bg: #ffffff;
--sg-header-fg: #000000;
--sg-row-alt-bg: #f1f1f1;
--sg-row-hover-bg: #ffe600; /* yellow hover band - unique tone */
--sg-selection-bg: #c20000; /* red selection - unique tone */
--sg-accent: #c20000;
--sg-focus-ring: 0 0 0 3px #c20000;
}
[data-theme='dark'] .high-contrast {
--sg-bg: #000000;
--sg-fg: #ffffff;
--sg-muted: #e6e6e6;
--sg-border: #ffffff;
--sg-header-bg: #000000;
--sg-header-fg: #ffff00; /* yellow header - text ink */
--sg-row-alt-bg: #1a1a1a;
--sg-row-hover-bg: #1f3b8a;
--sg-selection-bg: #ffff00;
--sg-accent: #ffff00;
--sg-focus-ring: 0 0 0 3px #ffff00;
}`}</code></pre>
</details>
</section>
<style>
.hc-shell { height: 100%; }
.hc-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.hc-title { font-size: 20px; font-weight: 700; margin: 0; color: var(--sg-fg, #0f172a); }
.hc-sub { font-size: 13px; color: var(--sg-muted, #64748b); margin: 4px 0 0 0; max-width: 70ch; line-height: 1.45; }
.hc-toggle {
display: inline-flex; border: 1px solid var(--sg-border, #cbd5e1); border-radius: 6px; overflow: hidden;
background: var(--sg-bg, #fff);
}
.hc-toggle button {
border: 0; background: transparent; color: var(--sg-fg, #0f172a);
padding: 6px 14px; font-size: 13px; cursor: pointer;
border-right: 1px solid var(--sg-border, #cbd5e1);
}
.hc-toggle button:last-child { border-right: 0; }
.hc-toggle button.on { background: var(--sg-accent, #2563eb); color: var(--sg-on-accent, #fff); font-weight: 600; }
/* High-contrast token preset. Light + dark variants. */
.hc-host-contrast {
--sg-bg: #ffffff;
--sg-fg: #000000;
--sg-muted: #1a1a1a;
--sg-border: #000000;
--sg-header-bg: #ffffff;
--sg-header-fg: #000000;
--sg-row-alt-bg: #f1f1f1;
--sg-row-hover-bg: #ffe600;
--sg-selection-bg: #c20000;
--sg-accent: #c20000;
--sg-focus-ring: 0 0 0 3px #c20000;
}
:global(html[data-theme='dark']) .hc-host-contrast {
--sg-bg: #000000;
--sg-fg: #ffffff;
--sg-muted: #e6e6e6;
--sg-border: #ffffff;
--sg-header-bg: #000000;
--sg-header-fg: #ffff00;
--sg-row-alt-bg: #1a1a1a;
--sg-row-hover-bg: #1f3b8a;
--sg-selection-bg: #ffff00;
--sg-accent: #ffff00;
--sg-focus-ring: 0 0 0 3px #ffff00;
}
/* Bump the cell-border weight in contrast mode so the grid lines are
not invisible at the higher contrast palette. */
.hc-host-contrast :global(.sv-grid-cell),
.hc-host-contrast :global(.sv-grid-column) {
border-color: var(--sg-border, #000) !important;
}
.hc-tokens {
border: 1px solid var(--sg-border, #cbd5e1);
background: var(--sg-bg, #fff);
border-radius: 8px;
padding: 8px 14px;
font-size: 12.5px;
}
.hc-tokens summary { cursor: pointer; font-weight: 600; color: var(--sg-fg, #0f172a); }
.hc-tokens pre {
margin: 8px 0 0 0;
overflow: auto;
font-family: ui-monospace, Menlo, monospace;
font-size: 11.5px;
background: var(--sg-header-bg, #f1f5f9);
border-radius: 4px;
padding: 8px 10px;
color: var(--sg-fg, #0f172a);
}
</style>More Themes & Styling examples
- Theming studio - Live token playground: brand color, density, radius, font, dark/light, zebra. Copy-ready CSS snippet, persists across reloads.
- Custom icon set - Replace the built-in glyphs with your own icon set. `icons` is a map keyed by icon name, so names you leave out keep their built-in glyph - a partial set is a complete answer. Toggle Ours / Mixed / Yours: Mixed overrides four icons and the rest of the chrome carries on unchanged.
- Theme integrations - Five design-system presets (Ant, MUI, Fluent, Base Web, shadcn) toggled by mapping --sg-* tokens. Side panel shows the exact CSS to paste into your app.