A drop-in accessibility preference engine. A user sets their color, text, motion, and focus preferences once — on your site, using their browser — and PrefKeeper remembers them and applies them automatically on return visits. No account, no login, no data leaving their device.
You control your site's design entirely. PrefKeeper only ever changes presentation — color, text sizing, motion, focus indicators — never markup, semantics, ARIA, or alt text. That stays your responsibility as the developer, same as it always has been.
Most accessibility color/theme tools either force one designer's idea of "accessible" on every visitor, or require a paid, hosted overlay service. PrefKeeper is neither: it's a free, open-source library that puts the choice in the visitor's hands — presets to get close, sliders to fine-tune exactly.
npm install prefkeeper
npx prefkeeper-setupThat second command creates a prefkeeper/ folder in your project:
prefkeeper/
├── fonts/ (the bundled accessible font, all weights)
├── prefkeeper-root.css (font-face rules + editable default colors)
└── panel.css (PrefKeeper's own UI styling -- DON'T EDIT THIS ONE)
Why a setup step, instead of everything working automatically from
node_modules? Two real reasons, not just convention:
node_modulesisn't guaranteed to exist wherever your site is actually deployed — many static hosts (Netlify, GitHub Pages, plain FTP) don't upload it at all. Files copied into your own project sidestep that.- If you use a bundler (Vite, webpack), PrefKeeper's own runtime font loading can't reliably guess where its files end up after your bundler repackages everything — this was confirmed with a real Vite production build, not assumed. Referencing the copied files directly in your own CSS avoids that guesswork entirely.
Re-running npx prefkeeper-setup later is safe: it never touches
fonts/ or prefkeeper-root.css again once they exist (so your edits
are never lost), but it does refresh panel.css every time, so it
never goes stale after a npm update prefkeeper.
Link all three files from your HTML — panel.css and
prefkeeper-root.css before your own site CSS, so your own rules can
still override anything PrefKeeper doesn't touch:
<link rel="stylesheet" href="prefkeeper/panel.css" />
<link rel="stylesheet" href="prefkeeper/prefkeeper-root.css" />
<link rel="stylesheet" href="your-site.css" />Then, anywhere in your JS:
import { initPrefKeeper } from 'prefkeeper';
document.getElementById('open-preferences-btn').addEventListener('click', () => {
initPrefKeeper();
});That's it — calling initPrefKeeper() mounts the preference panel as a
full-screen overlay, and on your visitor's next page load, PrefKeeper
automatically re-applies whatever they last saved.
PrefKeeper works by setting CSS custom properties on
document.documentElement. Your own CSS just needs to reference them —
prefkeeper-root.css already provides sensible defaults, so your site
looks correct even before anyone has opened the panel:
body {
background: var(--pk-background, #ffffff);
color: var(--pk-text, #222222);
font-family: var(--pk-font-family, Arial, sans-serif);
font-size: var(--pk-font-size, 100%);
line-height: var(--pk-line-height, 1.5);
}
a {
color: var(--pk-link, #0645ad);
}
.your-button {
background: var(--pk-primary, #0066cc);
color: var(--pk-on-primary, #ffffff);
transition: transform calc(var(--pk-reduce-motion, 1) * 0.3s) ease;
}
.your-button:focus {
outline-color: var(--pk-focus-outline-color, hsl(200, 100%, 50%));
outline-width: var(--pk-focus-outline-width, 3px);
outline-style: solid;
}See examples/vanilla/index.html for a
complete working example.
- Color — background, text, links, buttons (background and text independently), with presets for high/low contrast, dark/light mode, and common color-vision types
- Text — size, line height, letter spacing, word spacing, and an optional bundled dyslexia/low-vision-friendly font (Atkinson Hyperlegible Next)
- Motion — a reduced-motion toggle your own CSS transitions can key off of
- Focus — outline color and width for keyboard navigation
Add your own presets — or override the built-in ones — without forking anything by adding customPresets to the initPrefKeeper call:
initPrefKeeper({
customPresets: {
contrast: {
brand: {
label: 'Acme Corp Brand Colors',
values: {
background: { hue: 0, sat: 0, light: 100 },
text: { hue: 0, sat: 0, light: 10 },
primary: { hue: 280, sat: 70, light: 45 },
onPrimary: { hue: 0, sat: 0, light: 100 },
link: { hue: 280, sat: 70, light: 40 }
}
}
}
}
});Early (v0.1.2) but functional — the core preference panel, storage, import/export, and settings are built and tested. Published to npm. A browser extension (for preferences to follow a visitor across different sites) and a React wrapper are planned, not yet built.
MIT for the code. The bundled Atkinson Hyperlegible Next font is
licensed separately under the SIL Open Font License — see
src/assets/fonts/OFL.txt.