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()
  • mode is a Ref<'light' | 'dark' | 'system'> that defaults to 'system'. Writable directly, or via setMode.
  • isDark is a resolved ComputedRef<boolean>. It folds 'system' to the OS appearance: on web via matchMedia, on Lynx native via the host's theme global prop and themechanged global 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 the mode value. Calling toggle() while mode is 'system' pins to the opposite of whatever isDark currently 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 .dark CSS 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.