# Preferences

Define choices such as skin tone or density, save them, and keep components in sync.

Use `createPreference` to share a user preference across your app. It keeps components in sync when the preference changes and can save it between visits.

You can use it for a theme, like light or dark mode (see [`createColorScheme`](#choose-a-color-scheme)), or other user preferences like a preferred package manager or whether the sidebar is open.

## Create a Preference

Give the preference its allowed values, a default, and a `data-*` attribute. Here's a skin-tone preference:

```ts
import { createPreference } from "@ui-sdk/core"

const skinTone = createPreference({
  values: ["default", "light", "medium-light", "medium", "medium-dark", "dark"],
  defaultValue: "default",
  attribute: "data-skin-tone",
})

export { skinTone }
```

The attribute gives CSS a way to use the choice. [Apply Preferences](#apply-preferences) shows how to put it on the page. You decide what each value looks like.

### Read and Change the Value

Call `set` to change the choice and `get` to read its state:

```ts
skinTone.set("medium")
skinTone.get().value
```

After setting `"medium"`, `get().value` returns `"medium"`. `set` only accepts values from the allowed list. It returns `true` when the value changes, or `false` if it stays the same or the change is canceled.

### Respond to Changes

Add `onValueChange` to the preference's options to hear a proposed change:

```ts
onValueChange: (value, details) => {
  console.log(value, details.reason)
},
```

This runs before the value changes. The details include the reason, event, and source. Call `details.cancel()` if your app needs to keep the current value. [Handle Changes](/docs/core/kernel/handle-changes) covers these details.

### Follow Changes

Use `subscribe` to update something outside a framework component when the value changes:

```ts
const stop = skinTone.subscribe(({ value }) => {
  console.log(value)
})
```

Read the current state with `get()` when you set up. Call `stop()` when you no longer need the subscription. For a framework component, use [Read a Preference](#read-a-preference) below.

## Remember the Choice

Add [storage](/docs/core/storage) to save each choice and restore it on the next visit:

```ts
import { createLocalStorage, createPreference } from "@ui-sdk/core"

const skinTone = createPreference({
  values: ["default", "light", "medium-light", "medium", "medium-dark", "dark"],
  defaultValue: "default",
  attribute: "data-skin-tone",
  storage: createLocalStorage("skin-tone"),
})

export { skinTone }
```

This demo uses localStorage:

Choose a skin tone, then reload the page. Your choice is saved.

| Storage                | Use It For                                                    |
| ---------------------- | ------------------------------------------------------------- |
| `createLocalStorage`   | A choice that lasts between visits and is shared across tabs. |
| `createSessionStorage` | A choice that lasts while the tab is open.                    |
| `createCookieStorage`  | A choice the server needs to read to render content.          |

When subscribing restores a different saved value, `onValueChange` receives the reason `"restore"`. Changes received from another storage instance or tab report `"storage-sync"`. Which tabs can share a value depends on the storage.

If you render on the server, [apply saved preferences before the page paints](#show-a-preference-before-hydration) so styles use the saved choice from the start.

### When Nothing Is Saved

If there's no saved value, the preference starts with `defaultValue`. A saved value outside the allowed `values` uses that default too. For the skin-tone example, that's `"default"`.

### When Saving Is Unavailable

A browser can block storage or run out of space. The preference still changes and updates its subscribers, but the new choice may be lost on reload.

`skinTone.set(...)` returns whether the preference changed. If your interface needs to tell someone whether saving is available, [check the storage](/docs/core/storage#when-storage-is-unavailable).

### Remember a Sidebar's State

Here's another example. Use `createPreference` to remember whether a sidebar is open or closed. Define `sidebarPreference` in its own file:

```ts title="sidebar-preference.ts"
import { createLocalStorage, createPreference } from "@ui-sdk/core"

const sidebarPreference = createPreference({
  values: ["open", "closed"],
  defaultValue: "open",
  attribute: "data-sidebar",
  storage: createLocalStorage("sidebar-state"),
})

export { sidebarPreference }
```

Then read it in your sidebar component:

**React**

```tsx title="sidebar.tsx"
import { usePreference } from "@ui-sdk/react/preferences"
import { sidebarPreference } from "./sidebar-preference"

function Sidebar() {
  const { value } = usePreference(sidebarPreference)

  return <aside hidden={value === "closed"}>Sidebar content</aside>
}

export { Sidebar }
```

**Vue**

```vue title="sidebar.vue"
<script setup lang="ts">
import { usePreference } from "@ui-sdk/vue/preferences"
import { sidebarPreference } from "./sidebar-preference"

const sidebar = usePreference(sidebarPreference)
</script>

<template>
  <aside :hidden="sidebar.value === 'closed'">Sidebar content</aside>
</template>
```

**Svelte**

```svelte title="sidebar.svelte"
<script lang="ts">
  import { usePreference } from "@ui-sdk/svelte/preferences"
  import { sidebarPreference } from "./sidebar-preference"

  const sidebar = usePreference(sidebarPreference)
</script>

<aside hidden={sidebar.current.value === "closed"}>Sidebar content</aside>
```

**Solid**

```tsx title="sidebar.tsx"
import { usePreference } from "@ui-sdk/solid/preferences"
import { sidebarPreference } from "./sidebar-preference"

function Sidebar() {
  const sidebar = usePreference(sidebarPreference)

  return <aside hidden={sidebar().value === "closed"}>Sidebar content</aside>
}

export { Sidebar }
```

**Web Components**

```ts title="sidebar.ts"
import { sidebarPreference } from "./sidebar-preference"

class AppSidebar extends HTMLElement {
  #stop?: () => void

  connectedCallback() {
    const update = () => {
      this.hidden = sidebarPreference.get().value === "closed"
    }

    this.#stop = sidebarPreference.subscribe(update)
    update()
  }

  disconnectedCallback() {
    this.#stop?.()
  }
}

customElements.define("app-sidebar", AppSidebar)
```

```html
<app-sidebar>
  <aside>Sidebar content</aside>
</app-sidebar>
```

Call `sidebarPreference.set("closed")` from your toggle to close the sidebar, or `sidebarPreference.set("open")` to open it. The component updates when the preference changes, and the choice is saved for the next visit.

## Apply Preferences

Pass your preferences to your framework's [`Preferences` component](/docs/components/preferences) to apply their attributes and keep them current:

**React**

```tsx
import type * as React from "react"
import { Preferences } from "@ui-sdk/react/preferences"
import { skinTone } from "./preferences"

function App({ children }: { children: React.ReactNode }) {
  return <Preferences preferences={[skinTone]}>{children}</Preferences>
}

export { App }
```

**Vue**

```vue
<script setup lang="ts">
import { Preferences } from "@ui-sdk/vue/preferences"
import { skinTone } from "./preferences"
</script>

<template>
  <Preferences :preferences="[skinTone]">
    <RouterView />
  </Preferences>
</template>
```

**Svelte**

```svelte
<script lang="ts">
  import { Preferences } from "@ui-sdk/svelte/preferences"
  import { skinTone } from "$lib/preferences"

  let { children } = $props()
</script>

<Preferences preferences={[skinTone]}>
  {@render children()}
</Preferences>
```

**Solid**

```tsx
import { Preferences } from "@ui-sdk/solid/preferences"
import type { JSX } from "@solidjs/web"
import { skinTone } from "./preferences"

function App(props: { children: JSX.Element }) {
  return <Preferences preferences={[skinTone]}>{props.children}</Preferences>
}
```

**Web Components**

```html
<ui-preferences>
  <main>…</main>
</ui-preferences>

<script type="module">
  import "@ui-sdk/web/preferences"
  import { skinTone } from "./preferences.js"

  document.querySelector("ui-preferences").preferences = [skinTone]
</script>
```

See [Server Rendering](#server-rendering) to apply saved values before the page hydrates.

### On the Whole Page

Set `global: true` on a preference, then pass it to `Preferences`. Its attribute goes on `<html>` instead of the wrapper. For example, a density setting can apply to the whole page:

```ts
const density = createPreference({
  values: ["comfortable", "compact"],
  defaultValue: "comfortable",
  attribute: "data-density",
  global: true,
})
```

Without `global`, the attribute stays on its own `Preferences` element. Use separate preference instances when different parts of a page need their own choices.

### Read a Preference

Read the preference's current state in a component:

**React**

```tsx
import { usePreference } from "@ui-sdk/react/preferences"
import { skinTone } from "./preferences"

function CurrentTone() {
  const { value } = usePreference(skinTone)
  return <span>Selected tone: {value}</span>
}
```

**Vue**

```vue
<script setup lang="ts">
import { usePreference } from "@ui-sdk/vue/preferences"
import { skinTone } from "./preferences"

const tone = usePreference(skinTone)
</script>

<template>
  <span>Selected tone: {{ tone.value }}</span>
</template>
```

It returns a computed ref, so the template reads `tone.value` for the state's `value`.

**Svelte**

```svelte
<script lang="ts">
  import { usePreference } from "@ui-sdk/svelte/preferences"
  import { skinTone } from "$lib/preferences"

  const tone = usePreference(skinTone)
</script>

<span>Selected tone: {tone.current.value}</span>
```

**Solid**

```tsx
import { usePreference } from "@ui-sdk/solid/preferences"
import { skinTone } from "./preferences"

function CurrentTone() {
  const tone = usePreference(skinTone)
  return <span>Selected tone: {tone().value}</span>
}
```

**Web Components**

Use `get()` and `subscribe()` to read the value and keep your element current, as shown in [Follow Changes](#follow-changes).

## Use in a Primitive

Use `initialState` and `follow` when a primitive needs the preference in its own state. Start with `initialState` so server rendering and hydration agree:

```ts
state: () => ({ tone: skinTone.initialState }),
```

Then call `follow` from the root's `bind` to keep that state current. Return its cleanup function:

```ts
root: slot({
  attrs: (state) => ({ "data-skin-tone": state.tone.value }),
  bind: (_el, part) => skinTone.follow(part.store, "tone"),
}),
```

Call `skinTone.set(...)` from the primitive's API to change the choice.

## Choose a Color Scheme

`createColorScheme` uses `createPreference` internally. Its choice starts with these options:

```ts
const choice = createPreference({
  values: ["system", "light", "dark"],
  defaultValue: "system",
  attribute: "data-color-scheme",
})
```

That holds the choice. `createColorScheme` also reads the browser's preferred scheme and works out which scheme to use. For your app, use the helper:

```ts
import { createColorScheme, createLocalStorage } from "@ui-sdk/core"

const colorScheme = createColorScheme({
  storage: createLocalStorage("color-scheme"),
})

export { colorScheme }
```

It supports the same `set`, `subscribe`, `follow`, and cancelable `onValueChange` callback. Pass it to `Preferences` alongside your other preferences to apply its `data-color-scheme` attribute.

Call `colorScheme.set("dark")` to choose Dark. Call `colorScheme.set("system")` to follow the browser again. The default is `"system"`; an explicit Light or Dark choice stays the same when the browser's preference changes.

Choose Light or Dark. Choose Default (System), then change your system's color scheme to see it follow.

### Read the Color Scheme

`get()` returns three fields:

| Field      | What It Tells You                                                                      |
| ---------- | -------------------------------------------------------------------------------------- |
| `value`    | The app's choice: `"system"`, `"light"`, or `"dark"`.                                  |
| `system`   | The browser's preference: `"light"`, `"dark"`, or `"unknown"` when it isn't known yet. |
| `resolved` | The scheme to use: `value`, or `system` when the choice is `"system"`.                 |

For example, choosing Light while the browser prefers Dark gives `{ value: "light", system: "dark", resolved: "light" }`.

Use `value` for your CSS attribute, so CSS can handle `"system"` before JavaScript runs. [Style by Preferences](#style-by-preferences) shows how.

### Follow the Browser's Scheme

`onSystemValueChange` reports browser changes while something subscribes or follows:

```ts
const colorScheme = createColorScheme({
  onSystemValueChange: (value) => console.log(value),
})
```

Use `onValueChange` for the app's choice and `onSystemValueChange` for the browser's preference. Browser changes can't be canceled.

### Inside a Frame

Pass `view` to read an iframe's browser preference:

```ts
const colorScheme = createColorScheme({ view: frame.contentWindow! })
```

## Style by Preferences

When you pass a preference to `Preferences`, it adds the data attribute you chose with `attribute`. With `global: true`, the attribute goes on `<html>`. Otherwise, it goes on the `Preferences` element.

The density preference above uses `attribute: "data-density"` and `global: true`. When you call `density.set("compact")`, `Preferences` updates the attribute:

```html
<html data-density="compact">
  …
</html>
```

You can use that attribute in CSS or Tailwind to change how the page looks.

### CSS

Match the attribute's value in a selector. For example, reduce the space between rows in compact mode:

```css
.row {
  padding-block: 0.5rem;
}

[data-density="compact"] .row {
  padding-block: 0.25rem;
}
```

### Tailwind CSS

Define a variant that matches the attribute:

```css
@custom-variant compact (&:where([data-density="compact"], [data-density="compact"] *));
```

Then use it alongside your other utilities:

```html
<li class="py-2 compact:py-1">…</li>
```

### Color Scheme

Map `data-color-scheme` to the CSS `color-scheme` property. Use `light-dark()` to choose each color:

```css
:root,
[data-color-scheme="system"] {
  color-scheme: light dark;
}

[data-color-scheme="light"] {
  color-scheme: light;
}

[data-color-scheme="dark"] {
  color-scheme: dark;
}

:root,
[data-color-scheme] {
  --background: light-dark(oklch(1 0 0), oklch(0.145 0 0));
  --foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
}
```

For `"system"`, the browser chooses the scheme. Form controls and scrollbars follow it too. Declare the color variables on each element with its own scheme, so a dark sidebar can use different colors from the page.

For a page with one global scheme, Tailwind's `dark:` variant can handle both Dark and System:

```css
@custom-variant dark {
  &:where([data-color-scheme="dark"], [data-color-scheme="dark"] *) {
    @slot;
  }
  @media (prefers-color-scheme: dark) {
    &:where([data-color-scheme="system"], [data-color-scheme="system"] *) {
      @slot;
    }
  }
}
```

### Show Matching Content

For a few versions of some content, render each one and let CSS show the matching value. The skin-tone demo uses this for its greeting:

```tsx
import { skinTone } from "./preferences"

const waves = {
  default: "👋",
  light: "👋🏻",
  "medium-light": "👋🏼",
  medium: "👋🏽",
  "medium-dark": "👋🏾",
  dark: "👋🏿",
}

function Wave() {
  return (
    <span className="wave" role="img" aria-label="Waving hand">
      {skinTone.values.map((tone) => (
        <span key={tone} data-tone={tone} aria-hidden="true">
          {waves[tone]}
        </span>
      ))}
    </span>
  )
}

export { Wave }
```

```css
.wave [data-tone] {
  display: none;
}

[data-skin-tone="default"] .wave [data-tone="default"],
[data-skin-tone="light"] .wave [data-tone="light"],
[data-skin-tone="medium-light"] .wave [data-tone="medium-light"],
[data-skin-tone="medium"] .wave [data-tone="medium"],
[data-skin-tone="medium-dark"] .wave [data-tone="medium-dark"],
[data-skin-tone="dark"] .wave [data-tone="dark"] {
  display: inline;
}
```

The container supplies the accessible name. CSS shows one hand and hides the others.

## Server Rendering

The server can read cookies, but localStorage and sessionStorage are only available in the browser. Choose how to render based on what the preference changes:

| What Depends on the Preference                | How to Render It                           |
| --------------------------------------------- | ------------------------------------------ |
| Colors, spacing, or fonts                     | Style the attribute in CSS.                |
| A few versions of some content                | Render each version and show one with CSS. |
| Formatted text or content throughout the page | Read a cookie on the server.               |
| Content that can update after hydration       | Read the state with `usePreference`.       |

### Show a Preference Before Hydration

For a preference saved in localStorage or sessionStorage, pass it to `Preferences`. It renders an inline script that applies the saved choice before the page paints:

1. The server renders using `defaultValue`.
2. The script reads the saved choice and sets the preference's data attribute. CSS can use it immediately.
3. After hydration, `usePreference` and `follow` pick up the saved value.

For example, if someone saved `"medium"`, CSS sees `data-skin-tone="medium"` on the first paint. A label rendered from `usePreference` still starts with `"default"` and updates after hydration.

When your framework checks those attributes during hydration, [account for the script's changes](/docs/server-rendering#saved-preferences-and-storage). Pass `nonce` to `Preferences` if your Content Security Policy requires one for inline scripts.

For HTML you render yourself, `preferencesScript([skinTone])` generates the script. Place it inside the element that holds the preferences. For a single storage value, you can [render the storage's script](/docs/core/storage#show-a-value-before-hydration) instead.

### Read a Cookie on the Server

Use a cookie when the server needs the saved choice to render content, such as an install command for someone's preferred package manager. Inside your request handler, pass the `Cookie` header to the storage:

```ts
import { createCookieStorage, createPreference } from "@ui-sdk/core"

const packageManager = createPreference({
  values: ["npm", "pnpm", "yarn", "bun"],
  defaultValue: "npm",
  attribute: "data-package-manager",
  storage: createCookieStorage("package-manager", {
    cookie: request.headers.get("cookie"),
  }),
})
```

If the cookie contains `"pnpm"`, `packageManager.initialState.value` and `packageManager.get().value` both start at `"pnpm"`. A missing or invalid cookie uses `"npm"`.

In the browser, the storage reads `document.cookie`. [Create and share the preference for each request](/docs/server-rendering#read-a-cookie-on-the-server) so visitors don't share state. Components can then use the saved choice from their first render.

### Browser Preferences on the Server

When content depends on the system's color scheme, a supporting browser can send it in the `Sec-CH-Prefers-Color-Scheme` request header. Ask for it with these response headers:

```http
Accept-CH: Sec-CH-Prefers-Color-Scheme
Critical-CH: Sec-CH-Prefers-Color-Scheme
Vary: Sec-CH-Prefers-Color-Scheme
```

`Critical-CH` asks a supporting browser to retry a request that was missing the hint. `Vary` tells caches that the response depends on it. See [color-scheme client hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-CH-Prefers-Color-Scheme) for browser support.

Read the header with `colorSchemeOf` and pass it as `initial`:

```ts
import { colorSchemeOf, createColorScheme } from "@ui-sdk/core"

const colorScheme = createColorScheme({
  initial: colorSchemeOf(request.headers.get("Sec-CH-Prefers-Color-Scheme")),
})
```

`colorSchemeOf` returns `"unknown"` when the header is missing. Keep CSS handling for `"system"` so styles still follow the browser. Share the same `initial` value with the browser when hydrating.

Create preferences from a request's cookies or hints per request. This keeps one visitor's choices separate from another's.

## API Reference

### Preference Options

- `values` (`readonly Value[]`): Every value people can choose. A saved value that isn't one of them is ignored. Example: [Create a Preference](#create-a-preference).
- `defaultValue` (`NoInfer<Value>`): The value before anything is saved, and after a saved value is removed. Example: [Create a Preference](#create-a-preference).
- `attribute` (``data-${string}``): The attribute that shows the value, which has to start with `data-`, like `"data-skin-tone"`. Example: [Create a Preference](#create-a-preference).
- `global?` (`boolean`): Whether the attribute goes on `<html>`, for a preference the whole page styles by. Otherwise it goes on the `Preferences` element, for the part of the page inside it. Example: [On the Whole Page](#on-the-whole-page).
- `storage?` (`object`): Where the value is kept between visits, from `createLocalStorage`, `createSessionStorage` or `createCookieStorage`. Example: [Remember the Choice](#remember-the-choice).
- `onValueChange?` (`((value: Value, details: ChangeDetails<ChangeReason>) => void)`): Called before the value changes, with its reason. `details.cancel()` keeps the current one. Example: [Respond to Changes](#respond-to-changes).

### Preference State and Methods

Both `createPreference` and `createColorScheme` return these fields. A color scheme's state also includes [the browser's scheme and the resolved value](#read-the-color-scheme).

- `initialState` (`{ value: string; }`): What the server and the first client render show: `defaultValue`, or the value a cookie keeps. Example: [Use in a Primitive](#use-in-a-primitive).
- `get` (`() => { value: string; }`): The current state, the same object until the value changes. In the browser it's the saved value even before anything subscribes, for a render in the browser only. Hydrate from `initialState`. Example: [Read and Change the Value](#read-and-change-the-value).
- `set` (`(next: string, change?: { reason?: ChangeReason; event?: Event; }) => boolean`): Changes the value, through `onValueChange`, which can cancel it, and saves it. The reason defaults to `"imperative"`. Returns whether it changed. Example: [Read and Change the Value](#read-and-change-the-value).
- `subscribe` (`(listener: (state: { value: string; }) => void) => () => void`): Hears every change. The first subscriber reads back the saved value. Returns the function that stops it. Example: [Follow Changes](#follow-changes).
- `follow` (`<Key extends string>(store: PreferenceStore<Key, { value: string; }>, key: Key) => () => void`): Keeps `key` in a store set to the preference's state. Call it from a part's `bind`: the state starts at `initialState`, catches up a microtask later, once hydration is done, and follows every change after that. Returns the cleanup for `bind`. Example: [Use in a Primitive](#use-in-a-primitive).

### Preferences Component

- `nonce?` (`string`): The nonce your Content Security Policy gives inline scripts. Example: [Server Rendering](#server-rendering).
- `preferences` (`readonly PreferenceLike[]`): The preferences to show, from `createPreference` or `createColorScheme`. Example: [Apply Preferences](#apply-preferences).

### Color Scheme Options

`storage`, `global`, and `onValueChange` work the same way as in [`createPreference`](#preference-options). These options control the color scheme:

- `defaultValue?` (`ColorSchemeValue`): The scheme the app starts with, like a choice saved in a cookie that the server read. Example: [Choose a Color Scheme](#choose-a-color-scheme).
- `initial?` (`SystemColorScheme`): The browser's preference the server and the first client render assume, so hydration matches. Read the request's `Sec-CH-Prefers-Color-Scheme` hint with `colorSchemeOf`. Example: [Browser Preferences on the Server](#browser-preferences-on-the-server).
- `onSystemValueChange?` (`((value: "light" | "dark") => void)`): Called when the browser's preference changes, while something subscribes or follows. Example: [Follow the Browser's Scheme](#follow-the-browsers-scheme).
- `view?` (`Window`): The window to read, like an iframe's. Defaults to the page's window. Example: [Inside a Frame](#inside-a-frame).
