skip to content

In a Vue SFC, how does v-bind() inside a <style> block make CSS follow component state, and what does it compile to?

level: middleimportance: should knowfreq 45%

answer

  1. the stylesheet stays static
  2. custom properties do the moving
  3. inline style on the root
  4. quote an expression

basics

~20 s

Each v-bind() becomes var(--<hashed-name>) in static CSS, and the component sets that custom property as an inline style on its root element, updating it when the bound state changes. Expressions must be quoted, as in v-bind('theme.color').

solid answer

~40 s

The SFC compiler replaces `color: v-bind(color)` with `color: var(--<name>)`, where the name is derived from the component id and the expression (readable in development, hashed in production). The CSS itself stays **static**. At runtime the component sets each of those custom properties as an **inline style on its root element** (on each top-level element for a multi-root component) and re-sets them after updates when the bound state changes, so descendants inherit the new value. It works with the Options API and `<script setup>`, scoped or not; anything beyond a bare identifier must be quoted: `v-bind('theme.color')`. A number is written without a unit, so `v-bind(size)` with `size = 100` gives `100`, not `100px`.

code

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

const accent = ref('#3b82f6')
const size = ref(24)
</script>

<template>
  <div class="badge">
    <input v-model="accent" type="color" />
    <span class="dot" />
  </div>
</template>

<style scoped>
.dot {
  background: v-bind(accent);
  /* size is a number: multiply to add a unit */
  width: calc(v-bind(size) * 1px);
  height: calc(v-bind(size) * 1px);
}
.dot:hover {
  outline: 2px solid v-bind(accent);
}
</style>

go deeper

for a junior

Recall that v-bind() in a style block links a CSS value to component state and that expressions must be quoted.

for a middle

Explain the compile output: static var(--hashed) references plus inline custom properties set on the component root and updated after renders.

for a senior

Catch the practical traps: unitless numbers, null becoming initial, values invisible outside the root, and when a direct style write suits high-frequency changes better.

for a principal

Decide where theming lives: per-component v-bind() values versus shared design tokens as custom properties set higher in the tree, and the consistency each gives a design system.

## The feature Vue SFC `<style>` blocks accept a `v-bind()` CSS function that links a CSS value to component state: ```vue <script setup> import { ref } from 'vue' const theme = ref({ color: 'red' }) </script> <template> <p>hello</p> </template> <style scoped> p { color: v-bind('theme.color'); } </style> ``` Change `theme.value.color` and the paragraph changes color, with no `:style` binding in the template. The feature works in `<script setup>` components and in Options API components (bound to `data`, computed properties and so on), and it works whether the block is scoped or not. ## What it compiles to The key point, stated in the Vue docs: **the actual value is compiled into a hashed CSS custom property, so the CSS is still static.** 1. **In the stylesheet**, the compiler replaces each `v-bind(expr)` with `var(--<name>)`. In development builds the name is the component id plus an escaped form of the expression; in production it is a hash, adjusted so it never starts with a digit. 2. **In the component**, the compiler injects a call to an internal runtime helper that evaluates every bound expression and writes the results as custom properties. 3. **At runtime**, that helper sets each property as an **inline style on the component's root element**. For a multi-root component it sets them on every top-level element. Values are set on mount and re-applied after updates, through a watcher that runs after rendering. Because custom properties **inherit**, every descendant of the root sees the value, which is why a rule on an inner `p` can use it. ## Rules and edge cases - **Quoting.** A bare identifier, `v-bind(color)`, works unquoted. Any other JavaScript expression, including a property path like `theme.color`, must be quoted. - **Units.** The value is stringified as is. A number `100` becomes `100`, so `width: v-bind(size)` is invalid CSS unless `size` is a string such as `'100px'`, or you multiply in CSS with `calc(v-bind(size) * 1px)`. - **Null values.** `null` or `undefined` are written as `initial`; any other value that is neither a string nor a finite number triggers a development warning. - **Scope of the variable.** The property lives on the root and inherits downward, so it also reaches elements rendered by child components; they only use it if a rule references that exact generated name, which in practice means rules from this SFC. | Aspect | `v-bind()` in `<style>` | Rebuilding CSS text at runtime | |---|---|---| | Stylesheet | static, parsed once | regenerated on change | | What changes on update | one inline custom property on the root | the rule text itself | | Where values live | component state, read by the helper | string templates | | Works with scoped rules | yes | depends on the approach | ## Debugging a binding that does nothing - **Inspect the root element.** Its inline style should list the generated properties; in a development build the names are readable, such as `--<id>-accent`. - **Check the value.** A unitless number or an empty string in a length property produces invalid CSS that the browser silently drops. - **Check the element's position.** A rule for an element that is not under the component's root, for example content teleported elsewhere, may not see the inherited value. - **Check the quoting.** An unquoted path such as `v-bind(theme.color)` is not the documented form; write `v-bind('theme.color')`. ## When to use it - **Theme values from state or props**, such as a brand color or an accent coming from user settings. - **Values used in several rules** of the same component, where repeating a `:style` binding on every element would be noisy. - **Pseudo-elements and states** such as `::before` or `:hover`, which an inline style binding in the template cannot target, but a rule using `var(--x)` can. Avoid it for values that change every frame, such as pointer positions during a drag, where writing a style directly is cheaper to reason about; and remember that the value is only visible below the component's root. ## Summary `v-bind()` in `<style>` is a compile-time rewrite to CSS custom properties plus a small runtime that keeps those properties, set inline on the component's root, in sync with state. The stylesheet never changes; only variable values do.

  • Why does width: v-bind(size) do nothing when size is the number 120?
    The runtime writes the value with `String(value)`, so the custom property becomes `120` with no unit and `width: 120` is invalid CSS, which the browser drops. Store a string such as `'120px'`, or keep the number and write `width: calc(v-bind(size) * 1px)`.
  • Where exactly does the custom property live, and what follows from that?
    It is set as an inline style on the component's root element, or on each top-level element of a multi-root component. Descendants inherit it, so any rule in the SFC that targets an element under the root can read it; an element outside the root, such as a sibling of the component, never sees it.

It works like a printed form with blank fields: the stylesheet is printed once with named blanks, and the component only writes new values into the blanks on its own root element whenever its state changes.

saying these in an interview costs you the question

  • v-bind() in style regenerates and re-injects the stylesheet whenever state changes.
  • v-bind() in style only works in components using the Options API.
  • v-bind('theme.color') can be written without quotes like any identifier.
  • A numeric value gets a px unit added automatically.
  • The custom property is set on the document root, so the whole page can read it.