@gospore/theme
v1.0.165
Published
Provides the base styling primitives for the entire Design System: a Vue `<Theme>` component that toggles dark/light mode, runtime helpers for storing the preferred mode, a `<ThemeSwitcher>` dropdown, and the compiled token bundle (`theme.css`) that expos
Readme
@gospore/theme
Provides the base styling primitives for the entire Design System: a Vue
<Theme> component that toggles dark/light mode, runtime helpers for storing
the preferred mode, a <ThemeSwitcher> dropdown, and the compiled token bundle
(theme.css) that exposes colors, typography, spacing, elevation, etc.
Loading tokens
Import the bundled CSS once near your app entry point so the shared tokens and dark-mode rules are available to every component:
// main.ts
import "@gospore/theme/theme.css";Preventing a light first frame
Apps that let users persist dark mode should install the theme bootstrap before their other Vite plugins:
// vite.config.ts
import { createThemeBootstrapPlugin } from "@gospore/theme";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [createThemeBootstrapPlugin()],
});The plugin injects a synchronous script and the critical canvas colors into the
document head. It applies the same persisted auto, light, or dark contract
as <Theme> before application modules execute, preventing a light frame while
a dark application initializes. Keep <Theme> mounted as usual so it owns
subsequent system-mode changes and animated mode transitions.
Theme component
<script setup>
import { Theme } from "@gospore/theme";
</script>
<Theme mode="auto" accent-color="#0052CC">
<App />
</Theme><Theme> listens for system changes, reads the persisted mode, exposes a
theme provide/inject value, and toggles the global dark-mode attribute so
all Design System components react automatically.
When more than one independent provider is mounted, the most recently mounted
<Theme> owns the global attribute. A nested provider without an explicit
mode inherits its parent and does not take global ownership, which keeps
accent-only theme layers synchronized with the application shell. A nested
provider can opt into ownership with an explicit mode; removing that prop or
unmounting it restores the previous provider. Unmounting the last global owner
restores the document attribute that existed before the first provider mounted.
Rendered brand tones can differ from the raw accentColor input. Theme-generated
semantic surface / on-surface pairs and canvas-facing brand text, icons, and
borders maintain at least WCAG AA 4.5:1 contrast in light and dark modes; the
default blue aliases follow the same contract. Accent roles are also explicit:
--color-text-accent-<color>-<strength>-* is same-hue text for the normal
application surface, while --color-on-accent-<color>-<strength>-* is the
contrast-safe foreground for the matching --color-background-accent-*
surface. The available ramps do not provide a same-hue AA foreground for most
accent surfaces, so the paired on-accent tokens preserve the accent in the
surface and choose a mode-appropriate neutral foreground. --color-text-subtle-*
and --color-text-subtler-* use distinct accessible neutral steps so their
hierarchy does not depend on low-contrast text.
The neutral ramps use eased OKLCH lightness rather than equal numeric jumps.
This keeps adjacent steps perceptually ordered while giving the middle roles
stable contrast targets: neutral 600 is suitable for meaningful UI graphics,
700 is low-emphasis AA text on the normal application surface, and 800 is
the stronger secondary foreground. Component placeholders use their package
theme tokens mapped to 800, because they also need to remain readable on
interactive input surfaces; base-surface details such as adjacent-month calendar
dates can use 700.
Status colors follow the same paired-token rule. Use --color-text-<status>-*
for status-colored copy on the normal application surface. When text is rendered
on a matching status background, use its --color-<status>-on-soft-* or
--color-<status>-on-strong-* foreground instead. Interactive warning and danger
soft surfaces expose hovered and pressed foregrounds matching their background
states. Component packages map these semantic pairs through their own component
tokens instead of choosing palette colors in component styles.
Foreground-only UI such as required markers follows the same rule: component
tokens map to --color-text-danger-*, never to a background token used as text.
Component implementations consume those package-level tokens; direct global
color references belong in each component's theme.css, so themes can adapt a
component without overriding its implementation selectors.
Brand semantics are now role-based (surface, on-surface, border). Do not
rely on --color-brand-100..1000 as stable UI meaning tokens.
Brand Token Showcase Example
The Theme Playground includes an interactive brand-token example. You can change
accentColor live and inspect:
- The resulting semantic brand tokens (
surface,on-surface,border) plus legacy tokens (--color-brand-100..1000). - How multiple components (
Keyword,Button,Tabs,AvatarItem) consume those tokens.
Note: The live components follow the currently active global mode
(dark-mode). The token list still shows both light and dark token values.
Theme switcher
<script setup>
import { Theme, ThemeSwitcher } from "@gospore/theme";
import { ref } from "vue";
const mode = ref("auto");
</script>
<Theme :mode="mode">
<ThemeSwitcher v-model:mode="mode" />
</Theme>The switcher is a ready-made dropdown that persists the selected mode in a
host-only, one-year SameSite=Lax cookie via the exported readStoredMode,
writeStoredMode, and applyStoredModeToDocument helpers. HTTPS writes also
set Secure. Host-only scope intentionally avoids leaking the preference to
sibling subdomains and works on multi-level public suffixes without guessing a
registrable domain. On the first write, the helper also expires the shared-domain
cookie created by older releases before storing the host-only replacement. The
switcher emits update:mode and updates <Theme> when you use it with v-model.
Inside a <Theme>, the provider remains the sole owner of the global mode attribute;
the switcher only persists and emits the selection. A standalone switcher continues
to apply the selected mode directly for backwards compatibility.
ThemeSwitcher composes @gospore/dropdown-menu + DropdownItemRadio, so
import @gospore/dropdown-menu/dropdown-menu.css,
@gospore/button/button.css, and @gospore/radio/radio.css alongside the theme
tokens when you mount it.
Utility files
- Token source files live under
src/(basic.less,styles.less,colors.css,fonts.css,sizing.css,elevation.css,transitions.css,borders.css,layering.css). They are bundled intotheme.css; when contributing inside this repo you can import the individual files as needed. modeStorage.js: helpers for reading/writing the current theme mode.
Accessibility & motion
Theme respects the prefers-reduced-motion media query when animating dark /
light transitions. When users prefer reduced motion, the provider's transition
happens without a fade. Components may use short, property-scoped color
transitions for interaction feedback, so those properties can briefly
interpolate during a theme change. Accessibility and contrast contracts apply
to the settled light and dark modes.
