skip to content

In a Vue SFC, how do you use a <style module> block from the template and from <script setup>, and how does it differ from scoped?

level: middleimportance: nice to knowfreq 28%

answer

  1. hashed class names, not attributes
  2. an object injected into the instance
  3. a custom name via the attribute value
  4. a composable reads the same object

basics

~20 s

A <style module> block hashes its class names and exposes a map as $style in the template, or under a custom name with module="classes". In script setup, useCssModule() or useCssModule('classes') returns the same map. Scoped keeps class names and adds attributes.

solid answer

~40 s

`<style module>` compiles the block as CSS Modules: each class gets a hashed, collision-free name, and the mapping object is injected into the component. In the template you bind it: `:class="$style.red"`. Giving the attribute a value, `<style module="classes">`, injects the object under that key instead, `classes.red`. In `<script setup>` or `setup()`, `useCssModule()` returns the default `$style` object and `useCssModule('classes')` a named one; called outside setup it only warns and returns an empty object, and it is not supported in the global browser build. Compared with `scoped`, which keeps your class names and adds a `data-v` attribute to elements and selectors, modules isolate by **renaming** classes, so you must reference every class through the injected object rather than as a plain string.

code

vue · 18 lines
vue
<script setup lang="ts">
import { computed, useCssModule } from 'vue'

const props = defineProps<{ tone: 'info' | 'danger' }>()
const s = useCssModule('alert')

const classes = computed(() => [s.box, props.tone === 'danger' ? s.danger : s.info])
</script>

<template>
  <div :class="classes"><slot /></div>
</template>

<style module="alert">
.box { padding: 12px; }
.info { border-left: 4px solid steelblue; }
.danger { border-left: 4px solid crimson; }
</style>

go deeper

for a junior

Recall that <style module> exposes a class map as $style and that you bind classes with :class="$style.name".

for a middle

Explain the custom inject name, reading the map with useCssModule() in script setup, and why isolation comes from renaming rather than an attribute.

for a senior

Choose between scoped and module per component for how class names flow into script and children, and know the empty-object failure modes of useCssModule().

for a principal

Pick one styling convention for a codebase so reviewers do not juggle two isolation models, and document when the other one is allowed.

## Two ways to keep SFC styles local Vue SFCs offer two built-in isolation mechanisms for a `<style>` block: - **`scoped`**: class names stay as written; the compiler adds a `data-v-<hash>` attribute to the component's elements and to each selector. - **`module`**: the block is compiled as **CSS Modules**, and each class name is replaced by a hashed, unique one. The template refers to classes through an injected object rather than by their literal names. This answer covers Vue's side of CSS Modules: how the class map reaches the component and how you read it. How CSS Modules generate names, compose classes and handle global exceptions is the CSS Modules specification's business. ## Using `$style` in the template ```vue <template> <p :class="$style.red">This should be red</p> </template> <style module> .red { color: red; } </style> ``` The compiled component carries the class map, and Vue exposes it on the instance under the key `$style`. `$style.red` evaluates to the generated name, something like `_red_1x2y3`, so the class binding applies it. Plain `class="red"` would not match anything, because no rule named `.red` exists after compilation. ## Custom inject names A value on the `module` attribute renames the key: ```vue <template> <p :class="classes.red">red</p> </template> <style module="classes"> .red { color: red; } </style> ``` This is useful when a component has more than one module block, for example one for layout and one for typography, each under its own name. ## Reading the map in `<script setup>` The template can use `$style` directly, but script code has no `this`. The `useCssModule` API, imported from `vue`, returns the same object: | Call | Returns | |---|---| | `useCssModule()` | classes of the unnamed `<style module>` block (key `$style`) | | `useCssModule('classes')` | classes of `<style module="classes">` | Typical uses are building class lists in a computed, passing a generated class name to a child as a prop, or using it in a render function. Behaviour at the edges, from the runtime source: 1. Called outside `setup()` or `<script setup>`, it warns `useCssModule must be called inside setup()` in development and returns an empty object. 2. If the component has no module block, or none under that name, it warns and returns an empty object. 3. In the global (CDN `<script>` tag) build it is not supported and returns an empty object. ## Choosing between `scoped` and `module` | Aspect | `<style scoped>` | `<style module>` | |---|---|---| | How isolation works | attribute on elements and selectors | renamed class names | | How the template refers to a class | the literal name, `class="red"` | the map, `:class="$style.red"` | | Reaching a child's inner elements | `:deep()` | pass a generated class name, or use the spec's global syntax | | Class name usable from script | as written | via `useCssModule()` | | Readable class names in DevTools | yes | hashed | Neither is universally better: - `scoped` reads like ordinary CSS and suits most application components. - `module` makes every class reference explicit and traceable in code, avoids attribute-selector matching, and suits components that compute class lists in script or hand class names to children. - One SFC can mix blocks, for example a global `<style>`, a scoped block and a module block. ## Common mistakes - **Writing literal class names.** `class="red"` next to a module block matches nothing; bind `$style.red`. - **Typos fail quietly.** `$style.rde` evaluates to `undefined`, and a class binding simply adds nothing, so a misspelled key shows up only as missing styling. - **Reading the map too late.** `useCssModule()` belongs in setup code; calling it inside a later callback returns an empty object. - **Expecting scoped-style escapes.** `:deep()` and `:slotted()` are scoped-style pseudo-classes; in a module block, cross-component styling is done by passing generated class names or using the CSS Modules global syntax. ## Summary `<style module>` isolates by renaming classes and injecting a class map, reachable as `$style` (or a custom name) in the template and through `useCssModule()` in setup code. `scoped` isolates by attribute and keeps names; the choice is about how you want to reference classes, not about which is more isolated.

  • Why does class="red" not work in a component whose only style block is <style module>?
    The CSS Modules compile renames `.red` to a generated unique name, so no rule called `.red` exists in the output. The template must bind the generated name from the injected map, `:class="$style.red"`.
  • What does useCssModule() return when called inside a plain helper function after setup has finished?
    It needs the current component instance, which exists only while `setup()` or `<script setup>` runs. Called elsewhere, it logs `useCssModule must be called inside setup()` in development and returns an empty object, so every class lookup yields `undefined`. Call it during setup and pass the object along.

saying these in an interview costs you the question

  • <style module> works by adding a data-v attribute, just like scoped.
  • Classes from a module block can be used as plain strings, class="red".
  • useCssModule() can be called anywhere, even outside setup, to get the classes.
  • The injected object is always named $style; the name cannot be changed.
  • A single SFC can hold only one <style> block.