Skip to content

Composables

useAppearance

Light, dark, or follow the system — persisted, and applied as the dark class on <html>. This site's own theme toggle is this composable; the buttons below drive the page you are reading.

appearance: system · isDark: false

ReturnedType
appearanceRef<'light' | 'dark' | 'system'>The chosen mode, writable
isDarkComputedRef<boolean>What that resolves to right now
setAppearance(value)fnSet the mode
toggleAppearance()fnFlips between light and dark from the effective mode

system follows prefers-color-scheme live — change it in the OS while the page is open and the class flips without a reload.

Persistence

The choice is stored under iryx-ui:appearance and a stored preference always beats the startup default from the plugin or IApp. To avoid a flash of the wrong theme, set the class before first paint — a watcher runs after hydration, which is long enough for a dark-mode reader to get a full white screen on every navigation:

ts
// index.html, or a Nuxt/VitePress head script
;(() => {
  try {
    const stored = localStorage.getItem('iryx-ui:appearance') ?? 'system'
    const dark = stored === 'dark'
      || (stored === 'system' && matchMedia('(prefers-color-scheme: dark)').matches)
    document.documentElement.classList.toggle('dark', dark)
  }
  catch {}
})()

Read the same key the composable writes, so the two can never disagree.

Switching without a flash

Transitions are suppressed for the duration of a switch and restored immediately afterwards, so the border tokens do not visibly interpolate on the way between themes. Nothing is needed on your side.

Server rendering

Nothing here touches localStorage, matchMedia or document on the server, so isDark is always false in server-rendered markup. Two things follow from that.

A flash of the wrong theme, because the class lands after hydration. The pre-paint script above is the fix.

A hydration mismatch, if you branch on isDark — the server sent the light branch, and the first client render can already say dark. Render both and let CSS choose:

vue
<template>
  <SunIcon class="dark:hidden" />
  <MoonIcon class="hidden dark:block" />
</template>

Or hold the branch back until mounted, which is what this site's own toggle does:

vue
<script setup lang="ts">
const mounted = ref(false)
onMounted(() => (mounted.value = true))
</script>

<template>
  <span v-if="!mounted" class="size-4" />
  <MoonIcon v-else-if="isDark" />
  <SunIcon v-else />
</template>

Give the placeholder the same size as the icon it stands in for, or the layout shifts on hydration.