Reaching Into a Child
Use :deep(), :slotted() and :global() to cross the scoping boundary, and understand why each crossing is a cost.
.parent :deep(.child-class) { /* ... */ }Scoped styles stop at a child component's internals by design. Three selectors cross that line deliberately: :deep(), :slotted() and :global().
The three selectors
:deep() removes the hash from whatever is inside the parentheses. .a :deep(.b) compiles to .a[data-v-f3f3eg9] .b: the ancestor is still scoped to your component, the target is not. Use it for markup you do not control, such as a third-party widget, and for HTML inserted with v-html.
:slotted() goes the other way. Slot content is compiled in the parent and carries the parent's hash, so a plain scoped rule in the component that defines the slot will not match it. :slotted(p) matches it anyway. Reach for it in layout and prose wrappers that must constrain whatever is passed in.
:global() emits one rule with no hash at all, so you can define a global class without opening a second style block.
Why reaching in should be rare
A deep selector couples your component to another component's internal class names, and those names are not an API. The child renames a class in a patch release and your rule silently stops applying — nothing errors. Prefer a prop, a slot, or a CSS custom property the child reads. When you do reach in, keep it to one or two selectors and leave a comment saying why.
The older combinator forms ::v-deep and /deep/ are deprecated. The functional :deep() form is the current one.
Example
<!-- ReviewScore.vue -->
<script setup lang="ts">
import StarRating from './StarRating.vue'
const props = defineProps<{
score: number
reviews: number
}>()
</script>
<template>
<div class="review">
<star-rating :score="props.score" />
<span class="count">{{ props.reviews }} reviews</span>
</div>
</template>
<style scoped>
.review {
display: flex;
align-items: center;
gap: 0.5rem;
}
.count {
color: #666;
font-size: 0.875rem;
}
/* StarRating renders .star internally and exposes no size prop. */
/* Compiles to: .review[data-v-hash] .star { ... } */
.review :deep(.star) {
width: 1.25rem;
height: 1.25rem;
}
</style>When to use it
- Resize or recolour part of a third-party component that offers no prop for it.
- Set typography on Markdown rendered through v-html.
- Give slot content a reading width and vertical rhythm from the wrapper that owns the slot.
More examples
Constraining slot content with :slotted()
The component that owns the slot wants to set reading width and rhythm for text it did not write. Without :slotted() the rule silently does nothing, which is one of the most confusing failures in scoped CSS.
<!-- ProseBlock.vue -->
<script setup lang="ts">
const props = defineProps<{
heading: string
}>()
</script>
<template>
<section class="prose">
<h2 class="prose-heading">{{ props.heading }}</h2>
<slot />
</section>
</template>
<style scoped>
.prose {
max-width: 36rem;
}
.prose-heading {
margin: 0 0 0.75rem;
}
/* slot content carries the PARENT's hash, so a bare `p` rule would miss it */
:slotted(p) {
line-height: 1.6;
margin: 0 0 1rem;
}
:slotted(a) {
text-decoration: underline;
}
</style>Styling markup from v-html
Rendered Markdown from a CMS is the common case. The wrapper is yours and stays scoped; everything inside it needs :deep(), which is why these rules are grouped under one commented block.
<!-- ArticleBody.vue -->
<script setup lang="ts">
const props = defineProps<{
html: string
}>()
</script>
<template>
<!-- only render v-html from content you trust -->
<div class="body" v-html="props.html"></div>
</template>
<style scoped>
.body {
color: #222;
}
/* v-html output gets no data-v attribute, so every rule for it needs :deep() */
.body :deep(h2) {
font-size: 1.25rem;
margin-top: 1.5rem;
}
.body :deep(blockquote) {
border-left: 3px solid #ccc;
padding-left: 1rem;
margin-left: 0;
}
</style>A global class declared from one component
:global() keeps a rule that must apply outside the component in the file that explains it, instead of in a project-wide stylesheet where nobody can tell what sets the class. Use it for a handful of named states, not as a general escape.
<!-- ModalHost.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false)
function close(): void {
open.value = false
}
</script>
<template>
<div v-if="open" class="backdrop" @click="close">
<slot />
</div>
</template>
<style scoped>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
}
/* one global rule, kept next to the component responsible for it */
:global(.modal-open) {
overflow: hidden;
}
</style>
Discussion