skip to content

In a Vue 3 component with <style scoped>, why do its rules not style the content a parent passes into its <slot>, and what does :slotted() change?

level: middleimportance: should knowfreq 38%

answer

  1. who compiled the slot content
  2. whose data-v the elements carry
  3. a second attribute with an -s suffix
  4. the slot owner's styles opt in

basics

~20 s

Slot content is compiled in the parent's template, so its elements carry the parent's data-v attribute, not the child's. :slotted(.x) compiles to .x[data-v-child-s], and Vue adds that -s attribute to whatever the child renders through its <slot>.

solid answer

~30 s

Vue treats slot content as **owned by the parent** that wrote it: the markup between `<Card>` tags is compiled in the parent's template, so those elements carry the parent's `data-v` attribute. A scoped rule in `Card.vue` requires Card's attribute and therefore skips them, while the parent's own scoped rules still apply. To style slot content from the child, write `:slotted(.x)`. The compiler turns it into `.x[data-v-<card>-s]`, and when Card renders a `<slot>`, Vue adds a `data-v-<card>-s` attribute to the elements rendered there. So the child opts in explicitly, and only for elements that actually come through its slot.

code

vue · 17 lines
vue
<!-- InfoCard.vue -->
<template>
  <section class="card">
    <slot />
  </section>
</template>

<style scoped>
/* compiled: .card p[data-v-<card>], never matches the parent's <p> */
.card p {
  margin: 0;
}
/* compiled: p[data-v-<card>-s], matches <p> elements rendered through the slot */
:slotted(p) {
  margin: 0;
}
</style>

go deeper

for a junior

Recall that slot content is owned by the parent that wrote it, so the child's scoped styles skip it unless the child uses :slotted().

for a middle

Explain the -s slot scope attribute: what :slotted(.x) compiles to and how Vue marks elements rendered through a slot so the rule matches.

for a senior

Choose between :slotted(), :deep() and the parent's own rules deliberately, and avoid coupling a reusable child to its consumers' markup.

for a principal

Set a library convention for how components expose styling of consumer content, such as slot wrappers or CSS custom properties, instead of ad hoc slotted overrides.

## Whose elements are slot elements? A **slot** lets a parent pass template content into a child component: the child writes `<slot />` where the content should appear, and the parent writes the content between the child's tags. In the DOM the content ends up inside the child's markup, but in the code it was written, and compiled, in the **parent's** template. Scoped CSS is keyed on who rendered an element. Every element a component's template creates carries that component's `data-v-<hash>` attribute. Slot content is created by the parent's render code, so it carries the **parent's** attribute. The Vue docs state the rule directly: by default, scoped styles do not affect contents rendered by `<slot/>`, as they are considered to be owned by the parent component passing them in. The consequence has two sides: - The **parent's** scoped rules do style its slot content, because the elements carry the parent's id. - The **child's** scoped rules do not, even though the elements sit inside the child's DOM. ## What `:slotted()` compiles to The `:slotted()` pseudo-class is the child's explicit opt-in. The compiler rewrites it into an attribute selector with a suffixed id: ```css /* in Card.vue, <style scoped>, scope id data-v-c4 */ :slotted(.title) { font-weight: 600; } /* compiled */ .title[data-v-c4-s] { font-weight: 600; } ``` At runtime, when the child renders a `<slot>`, Vue marks the rendered slot content with an extra **slot scope id**, the child's id plus `-s`. (SFC tooling tells the template compiler whether the styles use `:slotted()` at all, and the marking is skipped when they do not.) Elements that came in through Card's slot therefore carry both the parent's `data-v-<parent>` and Card's `data-v-c4-s`, and the compiled rule matches them. | Rule in the child's `<style scoped>` | Compiled | Matches slot content? | |---|---|---| | `.title` | `.title[data-v-c4]` | No: slot elements lack `data-v-c4` | | `:slotted(.title)` | `.title[data-v-c4-s]` | Yes: slot elements get `data-v-c4-s` | | `:deep(.title)` | `[data-v-c4] .title` | Yes if under a Card-owned element, but also any child component's `.title` | ## Why a separate attribute instead of just `:deep()` `:deep()` also reaches slot content if it sits under one of the child's own elements, but it reaches **everything** below that anchor, including the inner elements of any nested components. `:slotted()` is narrower: 1. It matches only elements delivered through **this** component's slots. 2. It does not require an ancestor anchor, because the `-s` attribute is on the slot elements themselves. 3. It states the intent in the stylesheet: "I am styling what my consumers pass in." ## A worked render, step by step Take a parent `Page.vue` (scope id `data-v-p1`) that renders `<InfoCard><p class="note">Saved</p></InfoCard>`, and `InfoCard.vue` (scope id `data-v-c4`) whose template is `<section class="card"><slot /></section>`. 1. `Page` compiles the `<p>` as part of its own render code, so the element is created with `data-v-p1`. 2. `InfoCard` renders its `<section>` with `data-v-c4`, and at the `<slot />` position it renders the slot content supplied by `Page`. 3. Because `InfoCard`'s styles use `:slotted()`, the rendered slot content also receives `data-v-c4-s`. 4. The final DOM is `<section class="card" data-v-c4><p class="note" data-v-p1 data-v-c4-s>Saved</p></section>`. Now read the rules against that DOM: `Page`'s `.note[data-v-p1]` matches, `InfoCard`'s plain `.note[data-v-c4]` does not, and `InfoCard`'s `:slotted(.note)`, compiled to `.note[data-v-c4-s]`, does. When both components style the same property, the usual cascade decides: specificity first, then source order. ## Design judgment Styling slot content from the child couples the child to the consumer's markup. Common, defensible uses: - Normalising spacing of whatever is placed in a layout slot, such as removing the top margin of the first paragraph. - Typography defaults for a content region in a card or dialog. Where the child needs structured control, it is usually cleaner to render the wrapper element itself and let the parent pass only text or data, or to expose CSS custom properties. When the parent needs to style its own slot content, it does so with its own scoped rules, with no special syntax. ## Summary Slot content belongs to the component that wrote it, so it carries that component's scope attribute. A child styles it only through `:slotted()`, which compiles to a `data-v-<child>-s` attribute selector that Vue applies to elements rendered through the child's slots.

  • Does the parent's own scoped style block style the <p> it passes into a child's slot?
    Yes. The `<p>` is compiled in the parent's template, so it carries the parent's `data-v` attribute, and the parent's scoped rules such as `p[data-v-parent]` match it even though it renders inside the child's DOM.
  • Why prefer :slotted(p) over :deep(p) in the child?
    `:deep(p)` compiles to `[data-v-child] p` and reaches every `p` under any child-owned element, including paragraphs inside nested components. `:slotted(p)` compiles to `p[data-v-child-s]` and matches only elements delivered through this component's slots, which is the narrower, intended target.

saying these in an interview costs you the question

  • Slot content belongs to the child because it renders inside the child's DOM.
  • A child's scoped rules apply to its slot content automatically.
  • The parent cannot style its own slot content once it is passed to a child.
  • :slotted() makes the rule global, just without the scope attribute.
  • :slotted() and :deep() compile to the same selector.