@storybook-community/addon-css-user-preferences
v1.0.0
Published
Emulate CSS user preferences in Storybook
Readme
Storybook Addon: CSS User Preferences
This toolbar addon allows you to emulate CSS user preferences in Storybook.
Getting started
First, install the addon.
pnpm add -D @storybook-community/addon-css-user-preferencesAdd this line to your main.js file (create this file inside your Storybook config directory if needed).
module.exports = {
addons: ['@storybook-community/addon-css-user-preferences'],
};Configuration
By default, all CSS user preferences are set to the system default.
You can configure your own set of user preferences with the parameters.cssUserPrefs parameter:
// .storybook/preview.js
export const parameters = {
cssUserPrefs: {
"prefers-color-scheme": "light",
},
};JavaScript reads the same preference
Rewriting the CSSOM only reaches CSS. A component that branches in JavaScript —
a useMediaQuery hook, a theme library resolving its initial theme, a
reduced-motion check gating an animation — reads window.matchMedia, so the
addon patches that too while a preference is being emulated:
// with the toolbar set to dark
window.matchMedia('(prefers-color-scheme: dark)').matches // true
window.matchMedia('(prefers-color-scheme: light)').matches // falseThe patched MediaQueryList dispatches a change event when the toolbar
changes a preference, so a subscriber follows the toolbar instead of reading a
correct first value and then going stale. addEventListener('change'),
onchange, and the deprecated addListener all work.
Only the emulated part of a query is substituted. A preference left at its system default, and everything else in a compound condition, is still evaluated by the browser. A query shape the addon cannot express is answered with the real value rather than a guess.
Returning every preference to its system default puts the browser's own
window.matchMedia back, so a Storybook that is not emulating anything is left
untouched.
Cross-origin stylesheets
The rewriting works through the CSSOM, and the browser only exposes a
stylesheet's rules to a page that is allowed to read them. A <link> to another
origin without a crossorigin attribute — a Google Fonts or CDN stylesheet, say
— is fetched no-CORS, which leaves the sheet unreadable even when the server
sends access-control-allow-origin: *: origin-cleanliness follows the request
mode, not the response header alone.
Such a sheet is skipped, with one console.warn naming its href. Its
prefers-* conditions keep answering the real browser preference instead of the
toolbar. To opt one back in, ask for it with CORS:
<link rel="stylesheet" crossorigin="anonymous" href="https://cdn.example/theme.css" />Options
prefers-color-scheme
The prefers-color-scheme preference is used to detect if the user has requested a light or dark color theme.
@media (prefers-color-scheme: dark) {
.button {
background: #333;
color: #fff;
}
}
@media (prefers-color-scheme: light) {
.button {
background: #fff;
color: #555;
}
}prefers-contrast
The prefers-contrast preference is used to detect if the user has requested that the web content is presented with a higher or lower contrast.
.outline {
outline: 2px dashed black;
}
@media (prefers-contrast: more) {
.outline {
outline: 2px solid black;
}
}prefers-reduced-data
The prefers-reduced-data preference is used to detect if the user has requested the web content that consumes less internet traffic.
.hero {
background-image: url("images/hero.webp");
}
@media (prefers-reduced-data: reduce) {
.image {
background-image: url("images/[email protected]");
}
}prefers-reduced-motion
The prefers-reduced-motion preference is used to detect if the user has requested that the system minimize the amount of non-essential motion it uses.
.button {
animation: pulse 1s linear infinite both;
}
@media (prefers-reduced-motion) {
.button {
animation: none;
}
}prefers-reduced-transparency
The prefers-reduced-transparency preference is used to detect if the user has requested the system minimize the amount of transparent or translucent layer effects it uses.
.glass {
opacity: 0.5;
}
@media (prefers-reduced-transparency: reduce) {
.glass {
opacity: 1;
}
}