Skip to content

jmswrnr/userpref

Repository files navigation

NPM Version npm bundle size Static Badge Static Badge

userpref

Simple User Preferences for Web Apps.

  • πŸͺΆ < 1 KB size and 0 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.

Install

Add the userpref <script> tag as the first element inside <body>, this is important to avoid a flash of the incorrect theme.

ES Module (Recommended)

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>
  );
}

Manual

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.

API

Using the script tag will expose a userpref object on window.

window.userpref

All preferences are available in this object. The built-in preferences are:

  • window.userpref.theme: Theme Preference
  • window.userpref.motion: Motion Preference

Preference

Each preference is represented as an object, you can use this to get and set preferences:

  • user: The user preference
  • system: The system preference
  • resolved: The resolved preference value that is currently used (readonly)

Examples

Common Usage:

// 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;
});

CSS

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);
}

Custom Preferences

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.

React + TypeScript Example

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.

Example usage:

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>
    </>
  );
}

About

Simple User Preferences for Web Apps

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published