Skip to content

useTheme ​

The runtime theme engine. Set a brand color and its 50–950 ramp is written to :root for every component to pick up.

Open in demo
Open in demo

Import ​

ts
import { useTheme } from '@core/theme/useTheme'

Signature ​

ts
function useTheme(): {
  setPrimary: (hex: string) => void
  setSecondary: (hex: string) => void
  setAccent: (hex: string) => void
  setTheme: (payload: { primary?: string, secondary?: string, accent?: string }) => void
  toggleDark: () => void
  isDark: Readonly<Ref<boolean>>
  currentTheme: ComputedRef<ThemeSnapshot>
  exportTheme: () => string
  importTheme: (json: string) => void
}

Returns ​

MemberTypeDescription
setPrimary(hex: string) => voidGenerates the 50–950 ramp and writes --sa-primary-{stop} on :root.
setSecondary(hex: string) => voidThe same for --sa-secondary-{stop}.
setAccent(hex: string) => voidThe same for --sa-accent-{stop}.
setTheme(payload) => voidAny of the three at once. A key you leave out is left alone.
toggleDark() => voidFlips between light and dark.
isDarkReadonly<Ref<boolean>>Whether dark is on.
currentThemeComputedRef<ThemeSnapshot>The three colors, isDark, the density and the typography preset.
exportTheme() => stringThat snapshot as JSON.
importTheme(json: string) => voidApplies a snapshot. Throws TypeError on invalid JSON or a non-object.

ThemeSnapshot ​

KeyType
primarystring
secondarystring
accentstring
isDarkboolean
density'compact' | 'default' | 'comfortable'
typographyPreset'compact' | 'default' | 'comfortable'

Example ​

vue
<script setup lang="ts">
import { useTheme } from '@core/theme/useTheme'
import { ref } from 'vue'

const { setPrimary, toggleDark, isDark, exportTheme, importTheme } = useTheme()

const saved = ref('')

function save() {
  saved.value = exportTheme()
}

function restore() {
  if (saved.value)
    importTheme(saved.value)
}
</script>

<template>
  <div class="flex flex-wrap items-center gap-3">
    <input type="color" value="#1DB9C3" @input="setPrimary(($event.target as HTMLInputElement).value)">
    <button type="button" @click="toggleDark()">
      {{ isDark ? 'Light' : 'Dark' }}
    </button>
    <button type="button" @click="save()">
      Save theme
    </button>
    <button type="button" :disabled="!saved" @click="restore()">
      Restore
    </button>
  </div>
</template>

Notes ​

  • It is a module-scope singleton. Every caller gets the same state, so a color set in Settings is the color a widget three routes away reads.
  • The ramp is generated by generateShades, so one hex produces all eleven stops. You never write the stops yourself.
  • isDark is backed by VueUse's useColorMode, which writes the dark class on <html> and persists the choice under sa-color-mode. The colors themselves are not persisted here — useConfig stores them and the config wiring replays them on boot.
  • importTheme applies density and the typography preset as well, by calling useDensity().setDensity and useTypography().setScale. A key with the wrong type is skipped rather than throwing.
  • The setters no-op safely when there is no document, so importing this on the server does not break.