VisorVisor
Themes

Theming

Light and dark mode support in Visor — automatic system preference detection and manual theme toggle.

Overview

Visor ships light and dark themes out of the box via @loworbitstudio/visor-core. Both themes are CSS-only — no JavaScript required for the default behavior.

The system works at two levels:

  1. Automaticprefers-color-scheme: dark is respected by default. Users with dark system preferences get dark mode immediately.
  2. Manual toggle — Apply a class or data attribute to override the system preference.

Quick Start

Import the full token bundle in your app's root CSS file:

@import '@loworbitstudio/visor-core';

That's it. The bundle includes:

  • All primitive tokens (colors, spacing, typography)
  • All semantic tokens
  • Light theme applied to :root
  • Dark theme applied via @media (prefers-color-scheme: dark) and manual selectors

Using Stock Themes

Four stock themes ship with @loworbitstudio/visor-core as CSS subpath exports. Import the one you want and apply its class to your root element:

@import '@loworbitstudio/visor-core';
@import '@loworbitstudio/visor-core/themes/blackout';
<body class="blackout-theme">
  <!-- your app -->
</body>

Available themes:

ImportClass
@loworbitstudio/visor-core/themes/blackout.blackout-theme
@loworbitstudio/visor-core/themes/modern-minimal.modern-minimal-theme
@loworbitstudio/visor-core/themes/neutral.neutral-theme
@loworbitstudio/visor-core/themes/space.space-theme

Each stock theme is class-scoped — it only activates within elements that carry the theme class. Import the base @loworbitstudio/visor-core first; the stock theme import adds color, typography, and spacing overrides on top.

Automatic Dark Mode

No setup required. When a user's OS is set to dark mode, prefers-color-scheme: dark applies the dark token values automatically.

The dark theme activates on :root unless the consumer has explicitly forced light mode:

/* Applied automatically when system prefers dark */
@media (prefers-color-scheme: dark) {
  :root:not(.light):not(.theme-light):not([data-theme="light"]) {
    --text-primary: var(--color-gray-50);
    --surface-page: var(--color-gray-950);
    /* ...all adaptive tokens switch to dark values */
  }
}

Manual Theme Toggle

To override the system preference, apply one of the following to your <html> element (or any root container):

SelectorEffect
.darkForce dark mode
.theme-darkForce dark mode (legacy alias)
[data-theme="dark"]Force dark mode
.lightForce light mode (override dark system preference)
.theme-lightForce light mode (legacy alias)
[data-theme="light"]Force light mode

HTML Attribute Example

<!-- Force dark mode regardless of system preference -->
<html data-theme="dark">

<!-- Force light mode regardless of system preference -->
<html data-theme="light">

<!-- Let the system preference decide (default) -->
<html>

Class Example

<html class="dark">
  <!-- dark mode active -->
</html>

JavaScript Theme Toggle

The @loworbitstudio/visor-core package exports utilities for toggling themes programmatically:

import { applyTheme, getSystemTheme } from '@loworbitstudio/visor-core';

// Apply a theme
applyTheme('dark');   // adds .dark to <html>
applyTheme('light');  // adds .light to <html>

// Read the system preference
const systemTheme = getSystemTheme(); // 'light' | 'dark'

You can also pass a custom element as the second argument:

const container = document.getElementById('app');
applyTheme('dark', container);

Persisting User Preference

Combine applyTheme with localStorage to persist the user's choice:

import { applyTheme, getSystemTheme, type Theme } from '@loworbitstudio/visor-core';

function initTheme() {
  const stored = localStorage.getItem('theme') as Theme | null;
  const theme = stored ?? getSystemTheme();
  applyTheme(theme);
}

function toggleTheme() {
  const current = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
  const next: Theme = current === 'dark' ? 'light' : 'dark';
  applyTheme(next);
  localStorage.setItem('theme', next);
}

React Hook Example

import { useState, useEffect } from 'react';
import { applyTheme, getSystemTheme, type Theme } from '@loworbitstudio/visor-core';

function useTheme() {
  const [theme, setTheme] = useState<Theme>(() => {
    if (typeof window === 'undefined') return 'light';
    return (localStorage.getItem('theme') as Theme) ?? getSystemTheme();
  });

  useEffect(() => {
    applyTheme(theme);
    localStorage.setItem('theme', theme);
  }, [theme]);

  return { theme, setTheme };
}

// Usage
function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? 'Switch to Light' : 'Switch to Dark'}
    </button>
  );
}

Granular CSS Imports

Instead of the full bundle, you can import individual CSS files:

/* Primitive tokens only */
@import '@loworbitstudio/visor-core/primitives';

/* Semantic tokens only */
@import '@loworbitstudio/visor-core/semantic';

/* Light theme adaptive tokens */
@import '@loworbitstudio/visor-core/themes/light';

/* Dark theme adaptive tokens (manual selectors + media query) */
@import '@loworbitstudio/visor-core/themes/dark';

This is useful when you want to compose the token layers manually or when your build system handles CSS concatenation.

Token Architecture

All components in Visor reference CSS custom properties — never hardcoded values. This means every component automatically responds to theme changes with zero extra code.

/* A component referencing adaptive tokens */
.card {
  background: var(--surface-card);   /* white in light, gray-900 in dark */
  color: var(--text-primary);        /* gray-900 in light, gray-50 in dark */
  border: 1px solid var(--border-default); /* gray-200 in light, gray-700 in dark */
}

The 3-tier token architecture:

Primitives         Semantic              Adaptive
--color-gray-50    --text-primary        switches light ↔ dark
--color-gray-900   --surface-card        based on theme class
--color-blue-500   --border-default      or prefers-color-scheme

Consumers can override any tier. Overriding a primitive cascades to all semantic and adaptive tokens that reference it.