useLocalStorage
A reactive, fully typed value backed by localStorage. It reads the stored value on mount, falling back to initial,
writes through whenever current is assigned,
and follows changes made in other tabs.
It is SSR safe: the server renders initial and the stored value arrives on hydration. Every storage access is wrapped, so private mode, a full
quota or a hand-edited entry degrades instead of throwing.
null key makes the hook inert, which is how a component offers optional persistence without branching.
The return gained enabled and remove(),
and the new storage option selects the backing store. See useSessionStorage for the tab-scoped variant.Basic Usage
Read current reactively and assign to write through. Reload the page and the value is still here.
Persisted theme: system
Stored under sv5ui-docs-demo-theme.
<script lang="ts">
import { useLocalStorage } from 'sv5ui';
const theme = useLocalStorage('theme', 'system');
</script>
<p>Current theme: <strong>{theme.current}</strong></p>
<div class="flex gap-2">
{#each ['system', 'light', 'dark'] as t}
<button onclick={() => (theme.current = t)}>{t}</button>
{/each}
</div>Typed Objects
Pass a type parameter and an object as the initial value. Writing goes through the setter, so assign a new object rather than mutating a field in place.
fontSize 16px · compact false
<script lang="ts">
import { useLocalStorage } from 'sv5ui';
interface UserPrefs {
fontSize: number;
compact: boolean;
language: string;
}
const prefs = useLocalStorage<UserPrefs>('user-prefs', {
fontSize: 16,
compact: false,
language: 'en'
});
// Assign a whole object: the write goes through the setter, so mutating a
// field in place would not reach storage.
function toggleCompact() {
prefs.current = { ...prefs.current, compact: !prefs.current.compact };
}
</script>
<p>Font size: {prefs.current.fontSize}px</p>
<p>Compact: {prefs.current.compact}</p>
<button onclick={toggleCompact}>Toggle compact</button>Keys That Change
The key can be a getter, and the getter is tracked. Point it at a route, a document id or the signed-in user, and the hook re-reads the new slot the moment it changes. Give each person below a different density and switch between them: the values follow the user, with no reload and no manual bookkeeping.
sv5ui-docs-demo-prefs:ana · comfortable<script lang="ts">
import { useLocalStorage } from 'sv5ui';
let userId = $state('ana');
// The key can be a getter, and the getter is tracked. When it returns a
// different key the hook re-reads that slot, so two users on the same
// device never see each other's settings and no manual reload is needed.
const prefs = useLocalStorage(() => `prefs:${userId}`, { density: 'comfortable' });
</script>
<select bind:value={userId}>
<option value="ana">Ana</option>
<option value="minh">Minh</option>
</select>
<p>{prefs.current.density}</p>Optional Persistence
A null or undefined key makes the hook inert: nothing is read, nothing is written, and no storage event is listened
for. current keeps working as ordinary state, so a component with an optional storageKey prop has one code path rather than two. This is exactly what Resizable does.
not saved yet Turn Persist off, write a value, reload the page: it is gone. Turn it on, write again, reload: it is still there.
<script lang="ts">
import { useLocalStorage } from 'sv5ui';
let { storageKey }: { storageKey?: string } = $props();
// A null or undefined key makes the hook inert: nothing is read, nothing
// is written, and no storage event is listened for. current behaves as
// ordinary state. That is what lets a component offer opt-in persistence
// without branching between two code paths.
const layout = useLocalStorage(() => storageKey ?? null, [50, 50]);
</script>
<!-- enabled reports which mode the hook is in -->
{#if !layout.enabled}
<p>This layout will not be remembered</p>
{/if}
<!-- Resizable does exactly this with its own storageKey prop -->Removing a Value
remove() deletes the entry and returns current to initial without writing that value back. The distinction matters: assigning the default by hand would
leave a copy of today's default in storage, and a later change to the default would never reach
anyone who had pressed Reset.
{"fontSize":16,"compact":false}<script lang="ts">
import { useLocalStorage } from 'sv5ui';
const prefs = useLocalStorage('user-prefs', defaults);
</script>
<!-- remove() deletes the entry and falls back to initial, without writing
that fallback back into storage. The slot is genuinely empty afterwards,
so a later default change is picked up rather than shadowed by a copy of
the old one. -->
<button onclick={prefs.remove}>Reset to defaults</button>Custom Serializer
JSON does not handle a Set, Map or Date, so
hand over a pair of functions instead. A parse that throws is caught and the value falls
back to initial,
so a corrupted entry cannot take the page down.
[]<script lang="ts">
import { useLocalStorage } from 'sv5ui';
// JSON does not handle Set, Map or Date, so hand over a serializer.
const favorites = useLocalStorage('favorites', new Set<string>(), {
serializer: {
parse: (raw) => new Set<string>(JSON.parse(raw)),
stringify: (value) => JSON.stringify([...value])
}
});
function toggle(id: string) {
const next = new Set(favorites.current);
if (next.has(id)) next.delete(id);
else next.add(id);
favorites.current = next;
}
</script>
<!-- A parse that throws is caught and the value falls back to initial, so a
corrupted or hand-edited entry cannot break the page. -->Cross-tab Sync
When another tab writes the same key, the browser fires a storage event and the hook follows it. Events are filtered by key and by storage area, so a session hook
is never woken by a localStorage write and the other way round. The button below dispatches the event a second tab would produce.
theme system<script lang="ts">
import { useLocalStorage } from 'sv5ui';
// Default: another tab writing the same key updates this one, through the
// browser's storage event.
const theme = useLocalStorage('theme', 'system');
// Opt out when the value is per tab even though it is stored globally.
const token = useLocalStorage('session-token', '', { syncTabs: false });
</script>
<!-- The storage event is filtered by area as well as by key, so a session
hook is never woken by a localStorage write and the other way round. -->Pass syncTabs: false when the value happens to live in shared storage but belongs to one tab only.
Session Storage
The storage option switches the backing store to sessionStorage, which lives for the life of the tab. useSessionStorage is the same call with the option already applied.
See useSessionStorage for when each store is the right one.
import { useLocalStorage, useSessionStorage } from 'sv5ui';
// Same hook, different backing store.
const draft = useLocalStorage('draft', '', { storage: 'session' });
// useSessionStorage is the same call with the option already set, and its
// options type has storage removed so it cannot be contradicted.
const sameThing = useSessionStorage('draft', '');What Changed in v2.7.0
Existing calls are unaffected: every addition is opt-in.
| Area | Before |
|---|---|
Key type | string only |
Changing key at runtime | Not possible: one hook, one slot |
Turning persistence off | Branch between the hook and plain state by hand |
Clearing an entry | Assign the default back, which writes a copy of it |
Backing store | localStorage only |
Cross-tab events | Filtered by key |
Parameters
| Parameter | Type | Default |
|---|---|---|
key | StorageKey | (() => StorageKey) | - |
initial | T | - |
Options
| Option | Type | Default |
|---|---|---|
serializer | { parse, stringify } | JSON |
syncTabs | boolean | true |
storage | 'local' | 'session' | 'local' |
Return
| Property | Type |
|---|---|
current | T |
enabled | readonly boolean |
remove | () => void |