Theming
Dark Mode
useColorMode, the app-root class + remount contract, and what's not yet migrated to the semantic surface tier.
useColorMode() (from @vyui/kit) is a module-level singleton, so there is one color mode per app, shared across every call site, with no provide/inject needed.
import { useColorMode } from '@vyui/kit'
const { mode, isDark, setMode, toggle } = useColorMode()
modeis aRef<'light' | 'dark' | 'system'>that defaults to'system'. Writable directly, or viasetMode.isDarkis a resolvedComputedRef<boolean>. It folds'system'to the OS appearance: on web viamatchMedia, on Lynx native via the host'sthemeglobal prop andthemechangedglobal event (see Sparkling). A host that injects neither resolves to light.setMode(next)pins an explicit mode.toggle()flips between light and dark based on what's currently shown, not themodevalue. Callingtoggle()whilemodeis'system'pins to the opposite of whateverisDarkcurrently resolves to, because there's no "toggle from system" state.
The app-root contract
The color mode only takes effect where you bind it. Whatever element is the color-mode boundary for your app needs both of these:
<script setup lang="ts">
import { useColorMode } from '@vyui/kit'
const colorMode = useColorMode()
</script>
<template>
<view :class="{ dark: colorMode.isDark }" :key="colorMode.mode" class="…">
<!-- app -->
</view>
</template>
:class="{ dark: isDark }"flows the.darkCSS custom-property overrides to every descendant via inheritance.:key="mode"forces a full remount on every mode change. This is required, not decorative: Lynx native only applies a class change to freshly-mounted nodes, so toggling the class on an already-mounted tree silently does nothing without it. The trade-off is a brief remount blink and loss of component-local state in that subtree, so hoist any real app state above this boundary.
VyApp wires this contract for you (plus mounts OverlayRoot and sets --ui-radius), so use it instead of hand-rolling the above unless you need a custom root shell.
Not every component theme has been migrated onto the semantic surface tier yet: Island / IslandButton, and the avatar-group overlap ring, are still hardcoded light-only by design for now.