Web
@nano_kit/platform-web provides small reactive wrappers around browser APIs. The package is built on top of @nano_kit/store, so every helper returns a signal that can be used with effect, computed, framework integrations, query settings, or your own store utilities.
Instead of wiring browser event listeners by hand, use these helpers when you want Web API state to participate in Nano Kit reactivity.
Installation
Section titled “Installation”Install the package using your favorite package manager:
pnpm add @nano_kit/store @nano_kit/platform-webyarn add @nano_kit/store @nano_kit/platform-webnpm install @nano_kit/store @nano_kit/platform-webQuick Start
Section titled “Quick Start”import { BooleanCodec, effect} from '@nano_kit/store'import { $networkOnline, $pageVisible, localStored, mediaQuery} from '@nano_kit/platform-web'
const $dark = localStored('dark', false, BooleanCodec)const $wide = mediaQuery('(min-width: 768px)', false)
const stop = effect(() => { console.log({ dark: $dark(), online: $networkOnline(), pageVisible: $pageVisible(), wide: $wide() })})
$dark(true)
stop()Storage
Section titled “Storage”localStored and sessionStored create writable signals backed by localStorage and sessionStorage.
import { BooleanCodec, debounce } from '@nano_kit/store'import { localStored, sessionStored } from '@nano_kit/platform-web'
const $dark = localStored('dark', false, BooleanCodec)const $draft = sessionStored<string | null>('draft', '', debounce(300))
$dark(true)$draft('Hello')$draft(null) /* Removes sessionStorage.draft */syncedLocalStored and syncedSessionStored also listen for storage events, so the signal can react to changes from other browsing contexts.
import { effect } from '@nano_kit/store'import { syncedLocalStored } from '@nano_kit/platform-web'
const $language = syncedLocalStored('language', 'en')
const stop = effect(() => { document.documentElement.lang = $language()})All storage helpers support the same optional arguments as stored: default values, codecs, and setter rate limiters.
Assigning null or undefined removes the underlying localStorage or sessionStorage entry. If the signal has a default value, the signal falls back to that default after deletion.
Media Queries
Section titled “Media Queries”mediaQuery wraps window.matchMedia(...).
import { mediaQuery } from '@nano_kit/platform-web'
const $wide = mediaQuery('(min-width: 768px)', false)const $reducedMotion = mediaQuery('(prefers-reduced-motion: reduce)', false)The optional second argument is the fallback value used when window is not available.
Browser Properties
Section titled “Browser Properties”The package exports shared singleton signals for common browser state.
import { $devicePixelRatio, $fullscreen, $innerHeight, $innerWidth, $networkOnline, $outerHeight, $outerWidth, $pageVisible, $screenLeft, $screenOrientation, $screenTop, $scrollX, $scrollY} from '@nano_kit/platform-web'Window size and position:
$innerWidth$innerHeight$outerWidth$outerHeight$scrollX$scrollY$screenLeft$screenTop$devicePixelRatio
Page and device state:
$networkOnline$pageVisible$screenOrientation$fullscreen
Numeric window values use NaN when the browser value is not available. Boolean values use browser-friendly fallbacks: $networkOnline and $pageVisible start as true, and $fullscreen starts as false.
Permissions
Section titled “Permissions”permission creates a writable signal for the current Permissions API state. The value is 'granted', 'denied', 'prompt', Error, or undefined before the first result.
import { effect } from '@nano_kit/store'import { permission } from '@nano_kit/platform-web'
const $geolocationPermission = permission('geolocation')
const stop = effect(() => { const state = $geolocationPermission()
if (state === 'granted') { console.log('Geolocation is available') }})The argument can be a permission name string or a full PermissionDescriptor.
Geolocation
Section titled “Geolocation”$geolocation is backed by navigator.geolocation.watchPosition(...) and updates while the signal is active.
$staticGeolocation is backed by navigator.geolocation.getCurrentPosition(...) and reads a single snapshot when the signal starts.
import { effect } from '@nano_kit/store'import { $geolocation } from '@nano_kit/platform-web'
const stopLive = effect(() => { const result = $geolocation()
if (result && 'coords' in result) { console.log(result.coords.latitude, result.coords.longitude) }})Both signals can contain GeolocationPosition, GeolocationPositionError, or undefined.
Cookies
Section titled “Cookies”cookieStored and syncedCookieStored adapt a CookieStore-compatible object to Nano Kit storage signals.
import { JsonCodec } from '@nano_kit/store'import { cookieStored, syncedCookieStored } from '@nano_kit/platform-web'
const $theme = cookieStored<string | null>(cookieStore, { name: 'theme', maxAge: 60 * 60 * 24 * 30, path: '/'}, 'light')
const $profile = syncedCookieStored(cookieStore, 'profile', {}, JsonCodec)
$theme('dark')$theme(null) /* Calls cookieStore.delete({ name: 'theme', path: '/' }) */Use cookieStored when you only need reads and writes. Use syncedCookieStored when the cookie store supports change events and you want the signal to react to external cookie changes.
Assigning null or undefined deletes the underlying cookie through the provided CookieStore-compatible object. When cookieStored was created from options, deletion reuses the matching path, domain, and partitioned attributes so the browser removes the same cookie scope. If a default value was provided, the signal falls back to it after deletion.
For universal code, use the CookieStore$ injection token. By default it returns the browser cookieStore, so browser-only code does not need any setup.
import { inject } from '@nano_kit/store'import { CookieStore$, cookieStored} from '@nano_kit/platform-web'
export function Session$() { const cookieStore = inject(CookieStore$) const $session = cookieStored<string | null>(cookieStore, { name: 'session', path: '/', sameSite: 'lax' }, null)
return { $session }}During SSR and tests, provide CookieStore$ with VirtualCookieStore. It implements the CookieStore API from an incoming Cookie header and collects pending Set-Cookie headers from mutations.
import { InjectionContext, provide } from '@nano_kit/store'import { CookieStore$, VirtualCookieStore} from '@nano_kit/platform-web'
const cookieStore = new VirtualCookieStore( 'theme=dark; session=abc123', '/dashboard')
const context = new InjectionContext([ provide(CookieStore$, cookieStore)])
await cookieStore.set({ name: 'theme', value: 'light', path: '/', sameSite: 'lax'})
cookieStore.peek('theme') // 'light'cookieStore.drainSetCookieHeaders()// ['theme=light; Path=/; SameSite=Lax']Broadcast Channel
Section titled “Broadcast Channel”broadcasted creates a signal synchronized through BroadcastChannel. It is useful for transient cross-tab messages such as logout, refresh, or UI coordination events.
import { effect } from '@nano_kit/store'import { broadcasted } from '@nano_kit/platform-web'
const $authEvent = broadcasted<'logout' | 'refresh'>('auth')
const stop = effect(() => { if ($authEvent() === 'logout') { console.log('Log out this tab') }})
$authEvent('logout')broadcasted supports the same default value, codec, and setter rate limiter overloads as the storage helpers.
Locale
Section titled “Locale”browserLocale picks the best locale from a browser-like language container. In the browser, pass navigator directly.
import { browserLocale } from '@nano_kit/platform-web'
const locale = browserLocale(navigator, ['en', 'ru'], 'en')For universal code, use the Locales$ injection token. By default it returns navigator, so browser-only code does not need any setup.
import { inject } from '@nano_kit/store'import { Locales$, browserLocale} from '@nano_kit/platform-web'
export function Locale$() { const locales = inject(Locales$)
return browserLocale(locales, ['en', 'ru'], 'en')}On the server, provide Locales$ with parseLocales(...) from the request Accept-Language header.
import { provide } from '@nano_kit/store'import { Locales$, parseLocales} from '@nano_kit/platform-web'
const context = [ provide(Locales$, parseLocales(request.headers.get('accept-language')))]parseLocales sorts languages by q quality and keeps declaration order when qualities are equal. Empty headers fall back to { language: 'en', languages: ['en'] }.
User Agent
Section titled “User Agent”Use UserAgent$ when universal code needs the browser user agent string. In the browser it returns navigator.userAgent.
import { inject } from '@nano_kit/store'import { UserAgent$ } from '@nano_kit/platform-web'
export function Device$() { const userAgent = inject(UserAgent$)
return userAgent.includes('Mobile') ? 'mobile' : 'desktop'}On the server, provide UserAgent$ with the incoming User-Agent header.
import { provide } from '@nano_kit/store'import { UserAgent$ } from '@nano_kit/platform-web'
const context = [ provide(UserAgent$, request.headers.get('user-agent') ?? '')]