Skip to content

Dark Mode

Sakana Element has full built-in dark mode support with a Catppuccin Mocha inspired color palette. The theme system provides three modes — light, dark, and system — with automatic localStorage persistence and system preference detection.

NameCategoryDescriptionType
theme? ExposeCurrent theme settingComputedRef<'light' | 'dark' | 'system'>
isDark? ExposeWhether dark mode is currently activeComputedRef<boolean>
prefersDark? ExposeWhether the OS prefers dark modeRef<boolean>
prefers? ExposeThe OS color scheme preferenceRef<'light' | 'dark'>
setTheme? MethodSet theme to 'light', 'dark', or 'system'(theme: Theme) => void
toggleTheme? MethodToggle between light and dark() => void

Basic Usage

Use the useTheme composable to toggle between themes. The state is shared across all components and persisted in localStorage.

<template>
  <div class="demo-dark-mode">
    <div class="demo-dark-mode__status">
      <span>Theme: <code>{{ theme }}</code></span>
      <span>isDark: <code>{{ isDark }}</code></span>
    </div>
    <div class="demo-dark-mode__actions">
      <px-button @click="toggleTheme">Toggle Theme</px-button>
      <px-button type="primary" @click="setTheme('light')">Light</px-button>
      <px-button type="primary" @click="setTheme('dark')">Dark</px-button>
      <px-button type="info" @click="setTheme('system')">System</px-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useTheme } from 'sakana-element';
import { useData } from 'vitepress';
import { watch } from 'vue';

const { isDark, theme, toggleTheme, setTheme } = useTheme();
const { isDark: vpIsDark } = useData();

// Sync VitePress → useTheme (immediate: true aligns initial state)
watch(
  vpIsDark,
  (dark) => {
    if (dark !== isDark.value) setTheme(dark ? 'dark' : 'light');
  },
  { immediate: true },
);

// Sync useTheme → VitePress
watch(isDark, (dark) => {
  if (dark !== vpIsDark.value) vpIsDark.value = dark;
});
</script>

<style scoped>
.demo-dark-mode__status {
  display: flex;
  gap: 20px;
  margin-bottom: 16px;
  font-size: 14px;
}

.demo-dark-mode__status code {
  padding: 2px 6px;
  border: 1px solid var(--px-border-color-lighter);
  background: var(--px-fill-color-lighter);
}

.demo-dark-mode__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
</style>

Theme Modes

The setTheme function accepts three values:

ModeDescription
'light'Force light mode regardless of system preference
'dark'Force dark mode regardless of system preference
'system'Automatically follow the user's OS color scheme
ts
import { useTheme } from 'sakana-element'

const { setTheme, toggleTheme, isDark, theme } = useTheme()

// Set a specific mode
setTheme('dark')

// Toggle between light ↔ dark (ignores system)
toggleTheme()

// Read current state
console.log(theme.value)  // 'light' | 'dark' | 'system'
console.log(isDark.value)  // true | false

Persistence

When you call setTheme(), the chosen mode is saved to localStorage under the key px-theme. On page reload, the theme is automatically restored.

System Preference Detection

The useSystemTheme composable gives you read-only access to the user's OS color scheme. It updates reactively when the system preference changes.

vue
<script setup lang="ts">
import { useSystemTheme } from 'sakana-element'

const { prefersDark } = useSystemTheme()
</script>

<template>
  <p>System prefers dark: {{ prefersDark }}</p>
</template>

When to use which?

Use useTheme when you want to control the theme (toggle, persist, apply CSS classes). Use useSystemTheme when you only need to read the OS preference without changing anything — for example, to show a "Your system is in dark mode" indicator.

How It Works

When dark mode is active, Sakana Element adds the px-dark class to the <html> element. All component styles reference CSS custom properties (variables) that are redefined under .px-dark:

:root            →  light mode variables (default)
.px-dark, .dark  →  dark mode variable overrides

This means the theme switch is pure CSS — no component re-renders needed.

Customizing Colors

You can override any CSS variable for either theme. Define your overrides in your app's global CSS:

<template>
  <div class="demo-custom-vars">
    <div class="demo-custom-vars__section">
      <h4>Default Theme</h4>
      <div class="demo-custom-vars__row">
        <px-button type="primary">Primary</px-button>
        <px-button type="success">Success</px-button>
        <px-button type="warning">Warning</px-button>
        <px-button type="danger">Danger</px-button>
      </div>
    </div>

    <div class="demo-custom-vars__section custom-theme">
      <h4>Custom Theme (overridden variables)</h4>
      <div class="demo-custom-vars__row">
        <px-button type="primary">Primary</px-button>
        <px-button type="success">Success</px-button>
        <px-button type="warning">Warning</px-button>
        <px-button type="danger">Danger</px-button>
      </div>
    </div>

    <div class="demo-custom-vars__code">
      <code>.custom-theme { --px-color-primary: #e64553; ... }</code>
    </div>
  </div>
</template>

<script setup lang="ts">
</script>

<style scoped>
.demo-custom-vars {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.demo-custom-vars__section {
  padding: 16px;
  border: 2px solid var(--px-border-color-lighter);
  background: var(--px-bg-color);
}

.demo-custom-vars__section h4 {
  margin: 0 0 12px;
  font-size: 14px;
  color: var(--px-text-color-primary);
}

.demo-custom-vars__row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.demo-custom-vars__code {
  padding: 10px 14px;
  font-size: 12px;
  background: var(--px-fill-color-lighter);
  border: 1px solid var(--px-border-color-lighter);
}

.demo-custom-vars__code code {
  color: var(--px-text-color-regular);
}

/* Custom theme overrides — this is what we're demonstrating */
.custom-theme {
  --px-color-primary: #e64553;
  --px-color-primary-dark: #c73a47;
  --px-color-primary-light-3: #eb6a76;
  --px-color-primary-light-5: #f08f98;
  --px-color-primary-light-7: #f5b4ba;
  --px-color-primary-light-8: #f8c7cc;
  --px-color-primary-light-9: #fce3e5;

  --px-color-success: #209fb5;
  --px-color-success-dark: #1a8599;

  --px-color-warning: #df8e1d;
  --px-color-warning-dark: #bf7a19;

  --px-color-danger: #d20f39;
  --px-color-danger-dark: #b30d31;
}
</style>

Available Variable Categories

CategoryExample VariablesDescription
Primary--px-color-primary, --px-color-primary-darkBrand accent color
Success--px-color-success, --px-color-success-darkSuccess state color
Warning--px-color-warning, --px-color-warning-darkWarning state color
Danger--px-color-danger, --px-color-danger-darkError/danger state color
Info--px-color-info, --px-color-info-darkInformational state color
Background--px-bg-color, --px-bg-color-page, --px-bg-color-overlaySurface and page colors
Text--px-text-color-primary, --px-text-color-regular, etc.Typography colors
Border--px-border-color, --px-border-color-light, etc.Border and divider colors
Fill--px-fill-color, --px-fill-color-light, etc.Fill and background accent colors

TIP

Each semantic color (primary, success, etc.) has a -dark shade and -light-3 through -light-9 variants. Override these too for a consistent look across hover, disabled, and focus states.