skip to content

In a Vue SFC with <style scoped>, your rule for a third-party date picker's inner .dp-cell never applies; why, and how do you fix it without going global?

level: seniorimportance: must knowfreq 62%

answer

  1. whose attribute does .dp-cell carry
  2. anchor on an element you own
  3. the :deep() pseudo-class
  4. popups teleported out of your subtree

basics

~10 s

Your rule compiles to .dp-cell[data-v-yours], but the picker's inner cells carry the library's attribute or none, so it never matches. Write .picker-wrap :deep(.dp-cell), which compiles to .picker-wrap[data-v-yours] .dp-cell and stays under your wrapper.

solid answer

~40 s

Scoped CSS requires the **styled** element to carry your component's `data-v-<hash>` attribute. `.dp-cell` becomes `.dp-cell[data-v-yours]`, and the picker's inner cells are rendered by the picker, so they carry its attribute or none. The fix is the `:deep()` pseudo-class anchored on an element you own: `.picker-wrap :deep(.dp-cell)` compiles to `.picker-wrap[data-v-yours] .dp-cell`, so the attribute moves to your wrapper and the override applies only inside it. Check two more things: if the picker **teleports** its popup to `body`, the popup is no longer inside your wrapper and needs the library's own class or theming hook, or a `:global()` rule on a unique class; and prefer a documented theming API or CSS custom properties, because inner class names are not a public contract. The old `>>>`, `/deep/` and `::v-deep` combinator forms still compile but warn.

code

vue · 24 lines
vue
<script setup lang="ts">
import { ref } from 'vue'
import DatePicker from 'some-date-picker'

const day = ref<Date | null>(null)
</script>

<template>
  <div class="picker-wrap">
    <DatePicker v-model="day" />
  </div>
</template>

<style scoped>
/* compiled: .dp-cell[data-v-xxx], never matches the picker's inner cells */
.dp-cell {
  background: gold;
}

/* compiled: .picker-wrap[data-v-xxx] .dp-cell, matches cells under this wrapper only */
.picker-wrap :deep(.dp-cell) {
  background: gold;
}
</style>

go deeper

for a junior

Recall that a scoped rule only matches elements carrying your component's data-v attribute, and that :deep() is how you reach a child component's inner elements.

for a middle

Explain what .wrap :deep(.x) compiles to and why anchoring on your own element keeps the override local, compared with :global() and an unscoped block.

for a senior

Diagnose the failure in the DOM: check the target's attributes, catch teleported popups, handle specificity, and prefer the library's theming surface over inner class names.

for a principal

Treat deep overrides of third-party internals as coupling to an unversioned contract; push for a design-system theming API so upgrades do not silently break product styling.

## Why the rule never matches Inside `<style scoped>`, the SFC compiler rewrites every selector so that its **last compound selector** requires your component's scope attribute. A rule written as `.dp-cell { background: gold }` ships as: ```css .dp-cell[data-v-yours] { background: gold; } ``` The `.dp-cell` elements are rendered by the third-party picker component, not by your template. They carry the picker's own `data-v` attribute if the library uses scoped styles, or no attribute at all if it ships plain CSS. Either way they never carry **yours**, so the selector cannot match. Scoping is doing exactly its job: preventing your styles from leaking into other components. Only the picker's **root** element is an exception, because a single-root child's root carries the parent's attribute too; that is why a rule on the picker's outer class may work while a rule on an inner class silently does nothing. ## The fix: `:deep()` anchored on your own element The `:deep()` pseudo-class tells the compiler to put the attribute **before** the deep part of the selector instead of on its last compound. | Written in `<style scoped>` | Compiled to | Reach | |---|---|---| | `.picker-wrap :deep(.dp-cell)` | `.picker-wrap[data-v-yours] .dp-cell` | `.dp-cell` anywhere under your `.picker-wrap` | | `:deep(.dp-cell)` | `[data-v-yours] .dp-cell` | `.dp-cell` under any element of your component | | `:global(.dp-cell)` | `.dp-cell` | every `.dp-cell` in the page | The first row is the one to reach for. The attribute sits on an element your template renders, so the override is bounded to that subtree: other pages that use the same picker are unaffected. The second row works too but is broader, and the third is a truly global rule. ## Checks before you ship the override 1. **Is the target inside your DOM subtree?** Date pickers, selects and tooltips often render their popup through a teleport to `body`. A descendant selector anchored on `.picker-wrap` cannot match an element that is no longer its descendant. Use the library's documented hook for classing the popup, or a `:global()` rule on a class unique to your usage. 2. **Specificity.** The compiled selector `.picker-wrap[data-v-yours] .dp-cell` has its own specificity; if the library's rule is more specific, yours still loses. Match or beat it deliberately rather than reaching for `!important` first. 3. **Stability.** Inner class names are the library's implementation detail. A theming API, CSS custom properties the library reads, or props are safer than `:deep()` against a markup change in the next minor release. 4. **Syntax.** The Vue 2 era combinators `>>>` and `/deep/` and the combinator form `::v-deep .x` still compile in Vue 3.5, but the compiler warns that they are deprecated and asks for `:deep(<inner-selector>)`. ## Why not just remove `scoped`? - A plain `<style>` block is global: `.dp-cell` would restyle every picker in the application, including ones owned by other teams. - The same is true of `:global(.dp-cell)`, which drops the scope attribute entirely. - Both are legitimate for deliberate app-wide theming, but not for a local tweak. ## A related case: `v-html` Markup inserted with `v-html` also carries no scope attribute, for a different reason: it is parsed from a string rather than compiled from the template. The same pattern fixes it: `.article :deep(p)` compiles to `.article[data-v-yours] p`. ## Diagnosing it in the browser - Inspect the target element and read its attributes: if it lacks your `data-v-...` id, a plain scoped rule cannot reach it. - Look at the compiled rule in the styles panel to see where the attribute ended up. - Check whether the element sits under your wrapper at all, or was teleported elsewhere in the DOM. ## Choosing among the options | Option | Reach | Risk | |---|---|---| | Library theming API, props or its CSS custom properties | what the library supports | lowest: a documented contract | | `.wrap :deep(.inner)` | inner elements under your wrapper | breaks silently if the library renames `.inner` | | `:global(.unique-class .inner)` | every match in the page | leaks if the class is reused elsewhere | | Unscoped `<style>` block | every match in the page | app-wide side effects | Work down the table only as far as you have to. When you do use `:deep()`, leave a short comment naming the library version whose markup you targeted, so the next upgrade review knows where to look. ## Summary A scoped rule targeting a child's inner element fails because the element does not carry your attribute. `:deep()` moves the attribute onto an ancestor you own and keeps the override local; teleported popups, specificity and the stability of the library's markup are the three things that still break it.

  • The :deep() override works for the calendar grid but not for the dropdown popup. What is the likely cause?
    The popup is probably rendered through a teleport to `body`, so it is no longer a DOM descendant of `.picker-wrap`, and `.picker-wrap[data-v-yours] .dp-popup` cannot match. Use the library's documented way to add a class to the popup, or write a `:global()` rule on a class unique to this usage.
  • What happens if you write .picker-wrap >>> .dp-cell in a Vue 3.5 scoped style block?
    The compiler still rewrites it as a deep selector, `.picker-wrap[data-v-yours] .dp-cell`, but it warns that the `>>>` and `/deep/` combinators are deprecated and tells you to use `:deep()` instead. The same applies to the combinator form of `::v-deep`.
  • When is :global() the right tool instead of :deep()?
    When a rule genuinely must apply page-wide, or when the target sits outside your component's DOM subtree, such as a teleported overlay, and you control a unique class for it. `:global(.x)` compiles to plain `.x`, so it matches every such element in the page; keep the class specific to avoid restyling other usages.

saying these in an interview costs you the question

  • Adding !important to the scoped rule will make it reach the child's inner elements.
  • Removing scoped is the correct fix for styling one instance of a library component.
  • :deep(.dp-cell) makes the rule global, exactly like :global(.dp-cell).
  • A :deep() selector anchored on your wrapper also reaches the library's teleported popup.
  • >>> and /deep/ are the recommended Vue 3 syntax for deep selectors.