Home / Blog / How to Update :root CSS Variables with JavaScript in Vue
How to Update :root CSS Variables with JavaScript in Vue

How to Update :root CSS Variables with JavaScript in Vue

CSS variables (custom properties) let you store a value once on :root and reuse it everywhere throughout your stylesheets. The useful part for a Vue app is that you can change those values at runtime from JavaScript, and every CSS rule that reads them updates instantly. No re-render cycles, no class juggling, and no inline styles sprinkled across your HTML template.

This guide covers the two primary ways to do it: the raw DOM API (setProperty and getComputedStyle), and the Vue-idiomatic v-bind inside a component <style> block, which lets Vue wire reactive state straight into your CSS. Try the interactive live theme switcher below, inspect the API reference table, and learn how to implement both approaches cleanly in Vue 3.

Interactive Live Demo: Vue CSS Variable Theme Switcher

Try out the live theme switcher below. Switch between Light and Dark mode to observe how JavaScript updates :root CSS variables in real time:

Quick API Reference: Manipulating CSS Variables with JavaScript

In CSS, :root matches the document root element—the <html> tag. In JavaScript, this element is accessed via document.documentElement.

Task JavaScript Call Description & Notes
Set a :root variable document.documentElement.style.setProperty('--accent', '#42b883') Writes an inline style on <html>, overriding the value declared in CSS :root.
Read a variable value getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() Retrieves the computed string value. Always call .trim() to remove leading spaces.
Remove an override document.documentElement.style.removeProperty('--accent') Deletes the inline override, restoring stylesheet defaults.
Scope to an element elementRef.style.setProperty('--accent', '#ff5733') Modifies the variable only for that element and its descendant subtree.
// 1. Set a global CSS variable on :root
document.documentElement.style.setProperty('--main-color', '#42b983')

// 2. Read the current value (trimmed of whitespace)
const currentAccent = getComputedStyle(document.documentElement)
  .getPropertyValue('--main-color')
  .trim()

// 3. Remove the inline override
document.documentElement.style.removeProperty('--main-color')

Which Approach Should You Use?

Theming the entire page (Light/Dark mode, user color palettes)?
Update variables on :root using document.documentElement.style.setProperty(...). A single JavaScript execution instantly restyles all components reading those variables.

Styling a single component from its local reactive state?
Use v-bind in <style>. It automatically syncs Vue reactive state with custom properties without imperative DOM manipulation.

Need a bi-directional reactive Ref connected to a CSS variable?
Use VueUse's useCssVar composable for clean, declarative state management.

Approach 1: Manipulating :root CSS Variables via DOM API & Vue Watchers

When building global features like a theme switcher, setting custom properties on :root allows every component in your application to react immediately.

Vue 3 Composition API Example (<script setup>)

<template>
  <div class="p-6 max-w-md mx-auto bg-white rounded-xl shadow-md space-y-4">
    <h2 class="text-xl font-bold">Theme Switcher</h2>
    <p>Current Theme: <span class="font-semibold">{{ currentTheme }}</span></p>

    <button 
      @click="toggleTheme" 
      class="px-4 py-2 bg-[var(--main-color)] text-white rounded-md transition-colors"
    >
      Toggle {{ currentTheme === 'light' ? 'Dark' : 'Light' }} Mode
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref, watchEffect } from 'vue'

const themes = {
  light: {
    '--main-color': '#42b983',
    '--bg-color': '#ffffff',
    '--text-color': '#2c3e50',
  },
  dark: {
    '--main-color': '#42d392',
    '--bg-color': '#1a1a1a',
    '--text-color': '#f5f5f5',
  },
}

const currentTheme = ref<'light' | 'dark'>('light')

// Update :root CSS variables whenever reactive theme state changes
watchEffect(() => {
  if (typeof window !== 'undefined') {
    const root = document.documentElement
    const themeVariables = themes[currentTheme.value]

    for (const [key, value] of Object.entries(themeVariables)) {
      root.style.setProperty(key, value)
    }
  }
})

const toggleTheme = () => {
  currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'
}
</script>

<style>
:root {
  --main-color: #42b983;
  --bg-color: #ffffff;
  --text-color: #2c3e50;
}

body {
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s ease, color 0.3s ease;
}
</style>

Approach 2: The Vue-Idiomatic Way: v-bind() in Component <style>

For component-scoped styling driven by reactive state, Vue Single-File Components (SFCs) support v-bind() directly inside <style> tags. Under the hood, Vue compiles v-bind() into a scoped CSS custom property, keeping state and styles synchronized automatically.

<template>
  <div class="card">
    <h3>Dynamic Component Accent</h3>
    <input type="color" v-model="accentColor" class="cursor-pointer" />
    <button class="accent-button">Interactive Accent Button</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const accentColor = ref('#42b983')
</script>

<style scoped>
.card {
  padding: 1.5rem;
  border-radius: 8px;
  /* Vue automatically injects --hash-accentColor as a CSS custom property */
  border: 2px solid v-bind(accentColor);
}

.accent-button {
  background-color: v-bind(accentColor);
  color: #ffffff;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
}
</style>

When to use v-bind() vs setProperty()

  • Use v-bind() in <style>: When the state belongs strictly to a component (e.g., dynamic progress bars, component accent colors, user avatar borders).
  • Use document.documentElement.style.setProperty(): When setting app-wide theme tokens on :root that affect global layouts or unowned third-party components.

Approach 3: Reactive Binding with VueUse useCssVar

If you are using VueUse, the useCssVar composable provides a bi-directional reactive Ref bound directly to a CSS variable.

<script setup lang="ts">
import { useCssVar } from '@vueuse/core'
import { useTemplateRef } from 'vue'

// 1. Reactive ref bound to global :root CSS variable --main-color
const mainColor = useCssVar('--main-color')

const changeColor = () => {
  mainColor.value = '#ff5733' // Automatically executes setProperty under the hood
}

// 2. Scoped to a specific template ref
const containerRef = useTemplateRef<HTMLElement>('container')
const scopedColor = useCssVar('--local-accent', containerRef)
</script>

<template>
  <div ref="container">
    <button @click="changeColor">Update via VueUse Composable</button>
  </div>
</template>

Server-Side Rendering (SSR) & Nuxt 3 Caveats

⚠️ Important SSR Caution: document and getComputedStyle exist exclusively in browser environments. Calling document.documentElement during server-side pre-rendering in Nuxt 3 or Vue SSR will throw a ReferenceError: document is not defined.

To safely manipulate CSS variables in an SSR/Nuxt environment:

  1. Wrap DOM operations in onMounted():

    import { onMounted } from 'vue'
    
    onMounted(() => {
     document.documentElement.style.setProperty('--main-color', '#42b983')
    })
  2. Use client-side guards:
    if (import.meta.client) {
     document.documentElement.style.setProperty('--main-color', '#42b983')
    }
  3. Prefer v-bind() in <style>: Vue handles SSR rendering natively for v-bind(), injecting inline custom property declarations safely during hydration.

Performance Considerations & Best Practices

Manipulating CSS variables is high-performance because CSS custom properties do not trigger full DOM tree re-renders. However, frequent modifications during scroll or resize events can cause layout reflows.

1. Batch Multiple Variable Updates via cssText

When setting multiple variables at once, update cssText or group your modifications to minimize reflow passes:

const root = document.documentElement
root.style.cssText += `
  --main-color: #ff5733;
  --text-size: 18px;
  --border-radius: 10px;
`

2. Debounce Resize and Scroll Listeners

Use useDebounceFn from VueUse when modifying variables inside event listeners:

<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import { useDebounceFn } from '@vueuse/core'

const updateTextSize = () => {
  const newSize = window.innerWidth < 600 ? '14px' : '16px'
  document.documentElement.style.setProperty('--text-size', newSize)
}

const debouncedResize = useDebounceFn(updateTextSize, 200)

onMounted(() => {
  window.addEventListener('resize', debouncedResize)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', debouncedResize)
})
</script>

Frequently Asked Questions (FAQ)

How do I update a :root CSS variable with JavaScript?

Call document.documentElement.style.setProperty('--variable-name', 'value'). Because :root represents the root <html> element in JavaScript (document.documentElement), setting property values on it overrides stylesheet defaults across the whole document.

How do I read the current value of a CSS variable in JavaScript?

Use getComputedStyle(document.documentElement).getPropertyValue('--variable-name').trim(). Always chain .trim() on the returned string, as browsers may include leading whitespace in computed values.

What is the Vue way to bind CSS variables without touching the DOM?

Use v-bind() inside a component <style> block (e.g., color: v-bind(themeColor)). Vue automatically compiles reactive state variables into scoped CSS custom properties and updates them seamlessly.

Do JavaScript CSS variable updates work with server-side rendering or Nuxt 3?

document and getComputedStyle are browser APIs and will fail during SSR execution. Wrap DOM operations inside onMounted() hooks or import.meta.client guards. Using v-bind() in <style> is safe for SSR out of the box.

Should I switch themes by toggling a class or setting CSS variables?

Both techniques complement each other. Toggling a CSS class (e.g., <html class="dark">) works best for predefined themes configured in stylesheets. Setting CSS variables via JavaScript is ideal for user-defined, dynamic, or computed color choices.

Conclusion

Updating CSS variables with JavaScript provides a clean, responsive bridge between application logic and visual presentation. Whether you choose global :root updates via setProperty(), reactive component-scoped styles with v-bind(), or composable bindings via VueUse, Vue 3 makes dynamic styling intuitive.

You just wired reactive state into CSS with a few lines of Vue. The same pattern, applied across a real app, is what the Vue.js Master Class teaches end to end. Build a production-grade Vue app from scratch, one lesson at a time.

How do I update a :root CSS variable with JavaScript?

Call document.documentElement.style.setProperty('--name', 'value'). :root
is the <html> element, which JavaScript exposes as document.documentElement. Setting the property there overrides the value declared in your stylesheet, and every rule that reads the variable updates immediately.

How do I read the current value of a CSS variable in JavaScript?

Use getComputedStyle(document.documentElement).getPropertyValue('--name'). Call .trim() on the result, because the returned string can include leading whitespace. To read a variable scoped to a specific element, pass that element to getComputedStyle instead of document.documentElement.

What is the Vue way to bind a CSS variable without touching the DOM?

Use v-bind inside a component <style> block, for example background-color: v-bind(accent, where accent is reactive state. Vue compiles it to a CSS custom property and keeps it in sync with your state, so you avoid imperative DOM calls for anything driven by component data.

Do JavaScript CSS variable updates work with server-side rendering or Nuxt?

document and getComputedStyle exist only in the browser, so calling them during SSR throws. Run the code after the component mounts (onMounted) or behind a client-only guard. The v-bind in <style> approach is SSR-safe because Vue handles it for you.

Should I switch themes by toggling a class or by setting CSS variables from JavaScript?

Both work. Toggling a class (for example dark on <html>) suits a fixed set of themes defined entirely in CSS. Setting variables from JavaScript suits dynamic or computed values, like a user-picked accent color, where you do not know the value ahead of time. Many apps combine them: a class for the base theme, JavaScript-set variables for user overrides.

Start learning Vue.js for free

Comments

Latest Vue School Articles

5 Component Design Patterns to Boost Your Vue.js Applications

5 Component Design Patterns to Boost Your Vue.js Applications

5 essential Vue.js component design patterns, including branching components, slots usage, list organization, smart vs dumb components, and form handling - perfect for both Vue beginners and experienced developers looking to improve code maintainability and scalability.
Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Why I built a Nuxt + Jazz powered real time editor, how you can use it, and a list of takeaways on building with the help of AI.
VueSchool logo

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social