A lightweight and customizable Vue 3 theme switcher component that allows users to toggle between light and dark modes. Designed for modern interfaces, it persists the theme preference in localStorage and integrates seamlessly into any Vue 3 project or SSR environment (e.g. Nuxt 3).
- Features
- Installation
- Quick Start (SPA)
- Nuxt 4 / SSR Usage
- Component Registration Options
- Props
- Events
- Customization (Styles / Theming)
- Accessibility
- SSR Notes
- Development
- Changelog
- Contributing
- License
- Simple theme toggle: Switch between light and dark modes with a single click
- Persistent state: Automatically saves theme preference to localStorage (key:
theme) - Animated transition: Smooth visual toggle animation between states
- Bundled SVG icons: Pre-bundled light and dark mode icons (no external dependencies)
- Event emission: Emits
change-themeevent with the selected theme value - SSR compatible: Works in both SPA and Server-Side Rendered (Nuxt 3) environments
- Lightweight: Minimal footprint with CSS injected automatically
- Vue 3 Composition API: Built with modern Vue 3 patterns using composables
- Tree-shake friendly: Vue marked as external in library build
- TypeScript support: Includes type definitions
Using npm:
npm install @todovue/tv-theme-buttonUsing yarn:
yarn add @todovue/tv-theme-buttonUsing pnpm:
pnpm add @todovue/tv-theme-buttonYou must explicitly import the component's stylesheet in your application:
// main.ts or main.js
import { createApp } from 'vue'
import App from './App.vue'
// Import the component styles
import { TvThemeButton } from '@todovue/tv-theme-button'
import '@todovue/tv-theme-button/style.css' // Import styles
const app = createApp(App)
app.component('TvThemeButton', TvThemeButton)
app.mount('#app')import { createApp } from 'vue'
import App from './App.vue'
import TvThemeButton from '@todovue/tv-theme-button'
import '@todovue/tv-theme-button/style.css'
createApp(App)
.use(TvThemeButton) // enables <TvThemeButton /> globally
.mount('#app')Add the stylesheet to your nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@todovue/tv-theme-button/nuxt'
]
})Create a plugin file: plugins/tv-theme-button.client.ts (client-only is recommended for localStorage access):
import { defineNuxtPlugin } from '#app'
import TvThemeButton from '@todovue/tv-theme-button'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(TvThemeButton)
})Use anywhere in your app:
<template>
<TvThemeButton @change-theme="onThemeChange" />
</template>
<script setup>
const onThemeChange = (theme) => {
console.log('Current theme:', theme)
}
</script><script setup>
import { TvThemeButton } from '@todovue/tv-theme-button'
</script>
<template>
<TvThemeButton @change-theme="handleTheme" />
</template>| Approach | When to use |
|---|---|
Global via app.use(TvThemeButton) |
Many usages across app / design system install |
Local named import { TvThemeButton } |
Isolated / code-split contexts |
Direct default import import TvThemeButton from '@todovue/tv-theme-button' |
Single usage or manual registration |
| Prop Name | Type | Default | Description |
|---|---|---|---|
darkIcon |
String |
null |
Custom icon for dark mode. Accepts URL (http/https/data:), relative path (/path), or inline SVG string |
lightIcon |
String |
null |
Custom icon for light mode. Accepts URL (http/https/data:), relative path (/path), or inline SVG string |
buttonColor |
String |
null |
Custom background color for the switch button. Overrides the default theme background. |
knobColor |
String |
null |
Custom color for the sliding knob. |
sunColor |
String |
null |
Custom color for the sun icon (light mode icon). |
moonColor |
String |
null |
Custom color for the moon icon (dark mode icon). |
size |
String |
'md' |
Size of the button. Options: 'sm', 'md', 'lg'. |
square |
Boolean |
false |
If true, makes the button square instead of rounded. |
You can customize the theme icons in three ways:
<template>
<TvThemeButton
dark-icon="https://example.com/moon-icon.png"
light-icon="https://example.com/sun-icon.png"
@change-theme="handleTheme"
/>
</template><template>
<TvThemeButton
dark-icon="/icons/moon.svg"
light-icon="/icons/sun.svg"
@change-theme="handleTheme"
/>
</template><template>
<TvThemeButton
:dark-icon="darkSvg"
:light-icon="lightSvg"
@change-theme="handleTheme"
/>
</template>
<script setup>
const darkSvg = '<svg viewBox="0 0 24 24"><path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>'
const lightSvg = '<svg viewBox="0 0 24 24"><path d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg>'
</script>Note: If no custom icons are provided, the component uses built-in default icons.
- Initial theme is loaded from localStorage (key:
theme) - Falls back to
'dark'if no saved preference exists - Icons and animations are built-in
| Event name (kebab) | Emits (camel) | Payload Type | Description |
|---|---|---|---|
change-theme |
changeTheme |
'light' | 'dark' |
Emitted when user toggles theme. Returns current theme value. |
<template>
<TvThemeButton @change-theme="applyTheme" />
</template>
<script setup>
const applyTheme = (theme) => {
// Option 1: Apply CSS class to document
document.documentElement.setAttribute('data-theme', theme)
// Option 2: Update a Pinia/Vuex store
// useThemeStore().setTheme(theme)
// Option 3: Update CSS variables
if (theme === 'dark') {
document.documentElement.style.setProperty('--bg-color', '#1a1a1a')
document.documentElement.style.setProperty('--text-color', '#ffffff')
} else {
document.documentElement.style.setProperty('--bg-color', '#ffffff')
document.documentElement.style.setProperty('--text-color', '#000000')
}
}
</script>The component comes with default styles that can be customized using CSS variables or by overriding the scoped styles.
/* In your global CSS or component style */
.tv-theme-button {
/* Customize the switch background */
--switch-bg: #e0e0e0;
--switch-active-bg: #333;
--switch-size: 60px;
}
.tv-theme-icon {
/* Customize icon colors */
--icon-color: currentColor;
}<style>
/* Override component styles */
:deep(.tv-theme-switch) {
border-radius: 25px;
padding: 4px;
background: linear-gradient(to right, #667eea 0%, #764ba2 100%);
}
:deep(.tv-theme-icon) {
width: 24px;
height: 24px;
}
</style><script setup>
import { TvThemeButton } from '@todovue/tv-theme-button'
import { useDark, useToggle } from '@vueuse/core'
// Example with VueUse
const isDark = useDark()
const handleThemeChange = (theme) => {
isDark.value = theme === 'dark'
}
</script>
<template>
<TvThemeButton @change-theme="handleThemeChange" />
</template>- Keyboard accessible: The toggle button can be activated via click (mouse/touch)
- Visual feedback: Clear visual indication of current theme state
- Semantic HTML: Uses proper div/span structure with click handlers
For better accessibility, wrap the component or add context:
<div role="switch" :aria-checked="currentTheme === 'dark'" aria-label="Toggle dark mode">
<TvThemeButton @change-theme="currentTheme = $event" />
</div>- localStorage safe: Component uses
onMountedlifecycle hook to safely access localStorage only on client-side - No hydration issues: Initial state is set client-side, preventing SSR/CSR mismatches
- Vite compatible: SVG icons are bundled via Vite's
import.meta.glob(works in Vite + Nuxt) - Auto CSS injection: Styles are automatically injected when you import the library
- Client-side plugin recommended: For Nuxt 3, use
.client.tssuffix to ensure localStorage access
<!-- Add to your app.vue or layout -->
<script setup>
onMounted(() => {
const theme = localStorage.getItem('theme') || 'dark'
document.documentElement.setAttribute('data-theme', theme)
})
</script>| Feature | Status | Version |
|---|---|---|
Add defaultTheme prop |
Planned | 1.1.0 |
Add storageKey prop |
Planned | 1.1.0 |
| Custom icon slots/props | Planned | 1.2.0 |
| Add ARIA attributes internally | Planned | 1.2.0 |
| CSS variables for easy theming | Considering | 2.0.0 |
| Animation customization options | Considering | 2.0.0 |
| Multiple theme support (not just light/dark) | Considering | 2.0.0 |
Expose composable useThemeButton publicly |
Considering | 2.0.0 |
Clone the repository and install dependencies:
git clone https://github.com/TODOvue/tv-theme-button.git
cd tv-theme-button
yarn installyarn dev # Run development server with demo playground
yarn build # Build library for production
yarn build:demo # Build demo site for deploymentLocal demo is served from Vite using index.html + src/demo examples.
tv-theme-button/
├── src/
│ ├── components/
│ │ └── TvThemeButton.vue # Main component
│ ├── composable/
│ │ └── useThemeButton.js # Composable logic
│ ├── assets/
│ │ ├── icons/
│ │ │ ├── dark.svg # Dark mode icon
│ │ │ └── light.svg # Light mode icon
│ │ └── scss/ # Styles
│ ├── entry.ts # Library entry point
│ └── demo/ # Demo playground
├── dist/ # Build output
└── package.json
We welcome contributions! Please see our contributing guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT © TODOvue
See LICENSE for more information.
See CHANGELOG.md for version history and release notes.
Crafted with ❤️ for the TODOvue component ecosystem
