Vue 样式穿透 deep 的使用方法
scoped 做了什么
给 <style> 加上 scoped 之后,Vue 会做两件事:给当前组件模板里的每个元素加一个 data 属性(形如 data-v-7ba5bd90),同时给每条 CSS 选择器的最后一个选择器追加这个属性选择器。
所以写下这样的样式:
<style scoped>
.list .item {
color: red;
}
</style>
编译出来是:
.list .item[data-v-7ba5bd90] {
color: red;
}
注意属性选择器只加在最后一个 .item 上,.list 上没有。这就是「样式只作用于当前组件」的实现方式,也是穿透问题的根源。
为什么需要穿透
问题出在子组件的元素上。父组件模板里写 <MyInput />,编译时父组件的 hash 只会打到 <MyInput> 这个根元素上,子组件内部的元素带的是子组件自己的 hash。
于是父组件里想改子组件内部结构的样式就失效了:
<template>
<el-input class="my-input" />
</template>
<style scoped>
/* 无效:el-input 内部的 .el-input__inner 没有父组件的 hash */
.my-input .el-input__inner {
border-radius: 8px;
}
</style>
编译成 .my-input .el-input__inner[data-v-xxx],而实际 DOM 里那个 input 元素上根本没有 data-v-xxx,所以不匹配。
这在用组件库的时候几乎天天遇到——库的组件内部结构是它自己的,我们要定制样式就必须有办法让选择器绕过 scoped 的限制。
深度选择器的写法演进
历史上有过好几种写法,容易混乱,这里按 Vue 版本理一下。
Vue 2 时代:>>> 是原生 CSS 深度选择器,但 Sass/Less 这类预处理器解析不了它,所以有了别名 /deep/ 和 ::v-deep。
Vue 3 现在推荐的唯一写法是 :deep(),它是一个伪类,接受选择器作为参数:
<style scoped>
.my-input :deep(.el-input__inner) {
border-radius: 8px;
}
</style>
编译结果是把属性选择器加在 :deep() 之前的部分上:
.my-input[data-v-7ba5bd90] .el-input__inner {
border-radius: 8px;
}
这样 .my-input 是父组件自己的元素(带 hash 能匹配上),后代 .el-input__inner 不带 hash,就能匹配到子组件内部元素了。
已废弃的写法:>>>、/deep/、不带括号的 ::v-deep .foo。Vue 3 里后两者会输出警告,>>> 在预处理器里本来就不可用。看老代码遇到这些直接换成 :deep()。
必须给 :deep() 加前缀限定
这是最容易忽略的一点。可以直接写 :deep(.el-input__inner) 不带前缀:
<style scoped>
/* 危险 */
:deep(.el-input__inner) {
border-radius: 8px;
}
</style>
编译结果是 [data-v-xxx] .el-input__inner,属性选择器加在了一个「隐含的当前组件根元素」上。这看着还行,但实际上它会命中当前组件下所有层级的 .el-input__inner,包括嵌套很深的孙组件、通过插槽传进来的内容。在一个复杂页面里这就是全局污染。
正确做法是永远带一个自己的 class 作为限定:
<style scoped>
.my-form-item :deep(.el-input__inner) {
border-radius: 8px;
}
</style>
配合 Sass 嵌套
在 Sass 里写嵌套时 :deep() 的位置容易搞错:
// 正确
.wrapper {
:deep(.el-input__inner) {
border-radius: 8px;
}
:deep(.el-table) {
th {
background: #f5f7fa;
}
}
}
// 错误:& 展开后 :deep 在最前,等于没有前缀限定
:deep(.el-table) {
.wrapper & {
color: red;
}
}
:deep() 一旦出现,它右边的所有选择器都不再有 hash 限定。所以要限定作用域,前缀必须写在 :deep() 左边。
:slotted 和 :global
Vue 3 还提供了另外两个伪类,一起说清楚。
:slotted 用于给通过插槽传入的内容设置样式。默认情况下插槽内容带的是传入方(父组件)的 hash,不带当前组件的 hash,所以子组件的 scoped 样式管不到它:
<!-- 子组件 -->
<style scoped>
/* 无效,插槽内容没有本组件的 hash */
.card-body > p {
margin: 0;
}
/* 有效 */
:slotted(p) {
margin: 0;
}
</style>
:global 用于在 scoped 块里写一条全局样式,不加 hash:
<style scoped>
:global(body.modal-open) {
overflow: hidden;
}
</style>
这比再开一个不带 scoped 的 <style> 块更内聚,但要慎用,写进去的东西是真正全局的。
动态样式用 v-bind 更合适
如果只是想根据组件状态改一个颜色或尺寸,不必用 :deep() 硬改结构,v-bind() 配合 CSS 变量更干净:
<script setup>
const theme = ref('#409eff');
</script>
<template>
<div class="box">内容</div>
</template>
<style scoped>
.box {
/* 编译成 CSS 变量,值变化时不重新生成样式表 */
color: v-bind(theme);
}
</style>
组件库的定制也优先看它有没有暴露 CSS 变量。Element Plus 从 2.2 开始大量使用 --el-* 变量,改变量比 :deep() 覆盖选择器稳定得多——选择器依赖内部 class 名,库升级改了结构样式就失效,而变量是它的公开约定。
<style scoped>
.my-form {
--el-input-border-radius: 8px;
--el-color-primary: #722ed1;
}
</style>
权重不足的问题
:deep() 只解决了 hash 匹配,不解决 CSS 权重。组件库自己的样式可能选择器更长权重更高,这时候 :deep() 写对了样式还是不生效。
排查顺序是先在 devtools 里确认选择器有没有匹配上(匹配不上是 :deep() 用法问题),再看有没有被划掉(划掉是权重问题)。权重不够的解法是加长选择器而不是直接上 !important:
// 优先:加长自己的选择器
.my-form .el-form-item :deep(.el-input__inner) {
border-radius: 8px;
}
// 最后手段
.my-form :deep(.el-input__inner) {
border-radius: 8px !important;
}
另外样式的加载顺序也有影响。如果组件库的 CSS 是在业务样式之后被打包进去的,同权重下它会赢。这种情况检查一下入口文件里 import 'element-plus/dist/index.css' 的位置,它应该在业务样式之前。

还没有留言,来发表第一条吧~