Skip to main content

@renge-ui/vue

Composables for theme management.

Vue 3 composables for Renge theme management. Reactive state, provide/inject pattern, full token integration via CSS custom properties.

pnpm add @renge-ui/vue @renge-ui/tokens
v1.1.0Vue 3+GitHub →npm →

Overview

What is @renge-ui/vue?

A Vue 3-first adapter for Renge tokens.

@renge-ui/vue provides Vue 3 composables and provide/inject utilities for Renge theme management. It uses reactive refs and the Vue 3 Composition API to provide a clean, modern interface for consuming Renge tokens.

Key features: useRengeTheme() composable with reactive state, useRengeInject() for child component access, provide/inject pattern for component tree sharing, export of all Renge tokens and constants (PHI, FIBONACCI, GOLDEN_ANGLE).

Getting Started

Setup in three steps.

Install, import tokens, and use the composable.

1. Install Packages

Add both @renge-ui/vue and @renge-ui/tokens to your project.

pnpm add @renge-ui/vue @renge-ui/tokens

2. Import Tokens

Add CSS tokens to your main.js or global stylesheet.

// main.js
import '@renge-ui/tokens/renge.css';
import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');

3. Use Composable

Call useRengeTheme() in your root component to provide theme state to the component tree.

<template>
  <div :style="{ background: 'var(--renge-color-bg)', color: 'var(--renge-color-fg)' }">
    <p>Current Profile: {{ profile }}</p>
    <p>Current Mode: {{ mode }}</p>

    <button @click="switchProfile('earth')">Switch to Earth</button>
    <button @click="switchMode(mode === 'dark' ? 'light' : 'dark')">Toggle Mode</button>
  </div>
</template>

<script setup>
import { useRengeTheme } from '@renge-ui/vue';

const { profile, mode, switchProfile, switchMode } = useRengeTheme();
</script>

API Reference

useRengeTheme() composable

Complete composable reference with types and examples.

useRengeTheme()

Root composable that provides theme state and methods. Call this in your root component or layout.

<script setup>
import { useRengeTheme } from '@renge-ui/vue';

const {
  profile,        // ref<string> current profile
  mode,           // ref<'light' | 'dark'> current mode
  switchProfile,  // (name: string) => void
  switchMode,     // (m: 'light' | 'dark') => void
} = useRengeTheme();
</script>

useRengeInject()

Child component composable that accesses the provided theme context. Must be called inside a component tree that has called useRengeTheme().

<script setup>
import { useRengeInject } from '@renge-ui/vue';

const { profile, mode, switchProfile, switchMode } = useRengeInject();

// Will throw if useRengeTheme() was not called in parent
</script>

Type Definitions

export interface RengeThemeState {
  profile: string;
  mode: 'light' | 'dark';
}

export interface RengeThemeContext extends RengeThemeState {
  switchProfile: (name: string) => void;
  switchMode: (mode: 'light' | 'dark') => void;
}

🔜 Coming Soon: switchScale

Runtime base unit scaling with reactive updates across all spacing, radius, and dimension tokens.

Patterns

Common use cases

Real-world examples of theme switching in Vue applications.

localStorage contract: switchProfile and switchMode update the reactive ref and apply CSS to the DOM immediately. They do not automatically persist to localStorage or read from it on mount. You are responsible for calling them with stored values in onMounted and writing back in a watch. The examples below show the full pattern.

Persist Theme to localStorage

Save and restore theme from localStorage.

<script setup>
import { useRengeTheme } from '@renge-ui/vue';
import { onMounted, watch } from 'vue';

const { profile, mode, switchProfile, switchMode } = useRengeTheme();

// Load from localStorage on mount
onMounted(() => {
  const savedProfile = localStorage.getItem('renge-profile');
  const savedMode = localStorage.getItem('renge-mode');
  if (savedProfile) switchProfile(savedProfile);
  if (savedMode) switchMode(savedMode as 'light' | 'dark');
});

// Save changes to localStorage
watch(profile, (newProfile) => {
  localStorage.setItem('renge-profile', newProfile);
});

watch(mode, (newMode) => {
  localStorage.setItem('renge-mode', newMode);
});
</script>

Respect System Preference

Detect system dark mode and switch automatically on mount.

<script setup>
import { useRengeTheme } from '@renge-ui/vue';
import { onMounted } from 'vue';

const { switchMode } = useRengeTheme();

onMounted(() => {
  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    switchMode('dark');
  }
});
</script>

Theme Switcher Component

Complete profile switcher with all available profiles.

<template>
  <div style="display: flex; gap: 8px; flex-wrap: wrap;">
    <button
      v-for="p in profiles"
      :key="p"
      :class="{ active: profile === p }"
      @click="switchProfile(p)"
    >
      {{ p.charAt(0).toUpperCase() + p.slice(1) }}
    </button>
  </div>
</template>

<script setup>
import { useRengeInject } from '@renge-ui/vue';

const { profile, switchProfile } = useRengeInject();
const profiles = ['ocean', 'earth', 'twilight', 'fire', 'void', 'leaf'];
</script>

<style scoped>
button {
  padding: var(--renge-space-2) var(--renge-space-3);
  border: 1px solid var(--renge-color-border);
  border-radius: var(--renge-radius-2);
  background: var(--renge-color-bg-subtle);
  color: var(--renge-color-fg);
  cursor: pointer;
  transition: all var(--renge-duration-2) var(--renge-easing-ease-out);
}

button:hover {
  border-color: var(--renge-color-accent);
}

button.active {
  background: var(--renge-color-accent);
  color: var(--renge-color-bg);
  border-color: var(--renge-color-accent);
}
</style>

Watched Reactive Changes

React to theme changes with watchers for side effects.

<script setup>
import { useRengeTheme } from '@renge-ui/vue';
import { watch } from 'vue';

const { profile, mode } = useRengeTheme();

// Watch profile changes
watch(profile, (newProfile) => {
  console.log('Profile changed to:', newProfile);
  // Send analytics, update app state, etc.
});

// Watch mode changes
watch(mode, (newMode) => {
  console.log('Mode changed to:', newMode);
  // Update UI, theme provider, etc.
});

// Watch multiple
watch([profile, mode], ([newProfile, newMode]) => {
  console.log('Theme updated:', { profile: newProfile, mode: newMode });
});
</script>

Global Provide/Inject Setup

Provide theme globally from root component or App.vue.

<!-- App.vue -->
<template>
  <div>
    <ThemedContent />
  </div>
</template>

<script setup>
import { useRengeTheme } from '@renge-ui/vue';
import ThemedContent from './components/ThemedContent.vue';

const { profile, mode, switchProfile, switchMode } = useRengeTheme();
</script>

<!-- Any child component -->
<template>
  <button @click="switchProfile('twilight')">
    Current: {{ profile }}
  </button>
</template>

<script setup>
import { useRengeInject } from '@renge-ui/vue';

const { profile, switchProfile } = useRengeInject();
</script>

Integration

Framework patterns

Integrate Renge with Vue ecosystem tools and frameworks.

Nuxt 3 Setup

Use middleware or composable for SSR-safe theme initialization.

// composables/useTheme.ts
import { useRengeTheme } from '@renge-ui/vue';

export function useAppTheme() {
  const theme = useRengeTheme();

  if (process.client) {
    // Load from localStorage on client only
    const saved = {
      profile: localStorage.getItem('renge-profile'),
      mode: localStorage.getItem('renge-mode'),
    };
    if (saved.profile) theme.switchProfile(saved.profile);
    if (saved.mode) theme.switchMode(saved.mode as 'light' | 'dark');
  }

  return theme;
}

// app.vue
<script setup>
import { useAppTheme } from '~/composables/useTheme';
useAppTheme();
</script>

Vite + Vue 3 Setup

Standard Vite + Vue 3 project configuration.

// main.ts
import '@renge-ui/tokens/renge.css';
import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');

// App.vue
<template>
  <div id="app">
    <RouterView />
  </div>
</template>

<script setup>
import { useRengeTheme } from '@renge-ui/vue';
useRengeTheme();
</script>

Shared Composable Module

Create a reusable theme composable for consistency.

// composables/theme.ts
import { useRengeTheme } from '@renge-ui/vue';
import { onMounted } from 'vue';

export function useTheme() {
  const theme = useRengeTheme();

  onMounted(() => {
    // Restore from localStorage
    const saved = {
      profile: localStorage.getItem('renge-profile'),
      mode: localStorage.getItem('renge-mode'),
    };
    if (saved.profile) theme.switchProfile(saved.profile);
    if (saved.mode) theme.switchMode(saved.mode as 'light' | 'dark');
  });

  return theme;
}

// Usage in any component
<script setup>
import { useTheme } from '@/composables/theme';
const { profile, switchProfile } = useTheme();
</script>

Server-Side Rendering (SSR)

Handle theme initialization safely in SSR environments.

// app.vue
<template>
  <div :data-profile="profile" :data-mode="mode">
    <RouterView />
  </div>
</template>

<script setup>
import { useRengeTheme } from '@renge-ui/vue';
import { onBeforeMount, onMounted } from 'vue';

const { profile, mode, switchProfile, switchMode } = useRengeTheme();

// SSR: set from cookie or header
if (import.meta.env.SSR) {
  // Get theme from server context
  // switchProfile(serverTheme.profile);
  // switchMode(serverTheme.mode);
}

// Client: restore from localStorage
onMounted(() => {
  const saved = {
    profile: localStorage.getItem('renge-profile'),
    mode: localStorage.getItem('renge-mode'),
  };
  if (saved.profile) switchProfile(saved.profile);
  if (saved.mode) switchMode(saved.mode as 'light' | 'dark');
});
</script>

Best Practices

Vue-specific patterns

Recommended approaches for using Renge in Vue applications.

Use computed for derived state

<script setup>
import { useRengeInject } from '@renge-ui/vue';
import { computed } from 'vue';

const { profile } = useRengeInject();

// Good: computed derived state
const isDarkProfile = computed(() =>
  ['twilight', 'void'].includes(profile.value)
);

// Avoid: manual reactive updates
// let isDarkProfile = ref(false);
// watch(profile, p => { isDarkProfile.value = ['twilight', 'void'].includes(p); });
</script>

Prefer CSS variables in styles

<template>
  <!-- Good: CSS variables always in sync -->
  <div :style="{ background: 'var(--renge-color-bg)' }">
    Content
  </div>

  <!-- Acceptable: use rengeVars for computed values -->
  <div :style="{ padding: rengeVars.space['4'] }">
    Content
  </div>
</template>

<script setup>
import { rengeVars } from '@renge-ui/vue';
</script>

Watch changes with debouncing

<script setup>
import { useRengeTheme } from '@renge-ui/vue';
import { watch } from 'vue';
import { useDebounceFn } from '@vueuse/core';

const { profile } = useRengeTheme();

const saveTheme = useDebounceFn((newProfile) => {
  localStorage.setItem('renge-profile', newProfile);
}, 500);

watch(profile, saveTheme);
</script>

Error handling for useRengeInject

<script setup>
import { useRengeInject } from '@renge-ui/vue';

let theme;
try {
  theme = useRengeInject();
} catch (error) {
  // Handle case where provider is not available
  console.warn('useRengeTheme() was not called in parent');
  // Use fallback or default values
}
</script>

Tokens

Exported tokens

Direct access to Renge tokens for programmatic use.

rengeVars

Complete token object with type definitions.

<script setup>
import { rengeVars } from '@renge-ui/vue';

// Access any token
const spacing = rengeVars.space['4'];
const color = rengeVars.color.fg;
const radius = rengeVars.radius['2'];
</script>

Mathematical Constants

<script setup>
import { PHI, FIBONACCI, GOLDEN_ANGLE } from '@renge-ui/vue';

// PHI = 1.618033988749895
// FIBONACCI = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...]
// GOLDEN_ANGLE = 137.50776...
</script>

✨ WCAG 2.1 Level AA

All color profiles meet WCAG 2.1 Level AA contrast requirements. Theme switching maintains accessibility regardless of which profile is active.
Scale6.0