Simple User Preferences for Web Apps.
- πͺΆ
< 1 KB
size and0
dependencies. - ποΈ Framework agnostic.
- π« Saves user preference.
- π» Defaults to system preference.
- π Changes are updated on all tabs.
- 𧱠Supports custom preference definitions.
- π¨ Styles browser UI for light / dark theme.
- π Supports
light-dark()
CSS function. - π₯ No flash while loading theme.
Add the userpref <script>
tag as the first element inside <body>
, this is important to avoid a flash of the incorrect theme.
npm install userpref
With React or similar, you can use the source
named export:
import { source } from "userpref";
export default function ReactRootLayout() {
return (
<html>
<body>
<script
dangerouslySetInnerHTML={{
__html: source,
}}
/>
</body>
</html>
);
}
You could also grab the dist/userpref.js
build from npm and copy that inside a <script>
tag.
Warning
You will not get updates to this module if you manually copy the script.
Using the script tag will expose a userpref
object on window
.
All preferences are available in this object. The built-in preferences are:
window.userpref.theme
: Theme Preferencewindow.userpref.motion
: Motion Preference
Each preference is represented as an object, you can use this to get and set preferences:
user
: The user preferencesystem
: The system preferenceresolved
: The resolved preference value that is currently used (readonly)
// Setting User Preference:
userpref.theme.user = "dark"; // set user preference to dark theme
userpref.theme.user = "light"; // set user preference to light theme
userpref.theme.user = "system"; // set user preference to system theme
// Getting User Preference:
userpref.theme.user; // 'dark' | 'light' | 'system'
// Getting Resolved Preference:
userpref.theme.resolved; // 'dark' | 'light'
// Handle Preference Change:
window.addEventListener("userpref-change", (event) => {
const {
key, // 'theme' | 'motion' | ...
preference, // { user, system, resolved }
} = event.detail;
});
All preferences are set as data attributes on the <html>
element:
<html data-theme="dark" data-motion="reduced"></html>
This can be used in CSS queries:
:root {
--background: white;
}
[data-theme="dark"] {
--background: black;
}
[data-motion="reduced"],
[data-motion="reduced"] *,
[data-motion="reduced"] *::after,
[data-motion="reduced"] *::before {
transition-duration: 1ms !important;
animation-play-state: paused !important;
}
You can also use the new light-dark()
CSS function:
:root {
--background: light-dark(white, black);
}
Register custom preferences and their initial system preference using data attributes on the script tag:
<script
dangerouslySetInnerHTML={{
__html: source,
}}
data-audio="muted" // Register "audio" preference with default system preference of "muted"
/>
The default user preference will be "system"
which resolves to "muted"
.
// Setting Custom User Preference:
userpref.audio.user = "enabled";
userpref.audio.user = "muted";
userpref.audio.user = "system";
// Getting Custom Resolved Preference:
userpref.audio.resolved; // 'enabled' | 'muted'
// Setting Custom System Preference:
userpref.audio.system = "enabled";
userpref.audio.system = "muted";
Note
System preferences are automatically updated on theme
and motion
.
But you can set them for custom preferences.
If you need to access the preference using React, you can use a hook like this:
"use client";
import type { Preference, PreferenceChangeEvent } from "userpref";
import { useEffect, useState } from "react";
export const useReadPreference = (type: string) => {
const [preference, setPreference] = useState<Preference | null>(
typeof window === 'undefined'
? null
: Object.freeze({ ...window.userpref[type] }),
);
useEffect(() => {
const handleChange = (event: PreferenceChangeEvent) => {
if (event.detail.key === type) {
setPreference(Object.freeze({ ...event.detail.preference }));
}
};
window.addEventListener('userpref-change', handleChange);
return () => {
window.removeEventListener('userpref-change', handleChange);
};
}, [type]);
return preference;
};
Note
This example hook is only intended for reading the preferences. This is because we create a new object to easily re-render in React on change.
export function ToggleTheme() {
const theme = useReadPreference("theme");
return (
<>
<div>Current theme: {theme.resolved}</div>
<button onClick={() => (window.userpref.theme.user = "dark")}>
Use Dark Theme
</button>
<button onClick={() => (window.userpref.theme.user = "light")}>
Use Light Theme
</button>
<button onClick={() => (window.userpref.theme.user = "system")}>
Use System Theme
</button>
</>
);
}