Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/transition-reusable-scoped-style.md
1---2title: Avoid Scoped Styles in Reusable Transition Components3impact: MEDIUM4impactDescription: Scoped styles in transition wrapper components won't apply to slotted content, breaking the transition animation5type: gotcha6tags: [vue3, transition, scoped-css, slot, reusable-component]7---89# Avoid Scoped Styles in Reusable Transition Components1011**Impact: MEDIUM** - When creating reusable transition wrapper components, using `<style scoped>` will prevent the transition CSS classes from applying to slotted content. Scoped styles only affect elements directly in the component's template, not content passed through slots. Your transition animations will silently fail.1213## Task Checklist1415- [ ] In reusable transition components, use `<style>` without `scoped`16- [ ] Alternatively, use unique class name prefixes to avoid global conflicts17- [ ] Or use CSS modules with `:global()` for transition classes18- [ ] Test that transitions work when component is used in different contexts1920**Problematic Code:**21```vue22<!-- MyFadeTransition.vue -->23<template>24<Transition name="my-fade">25<slot />26</Transition>27</template>2829<!-- BAD: Scoped styles won't apply to slot content! -->30<style scoped>31.my-fade-enter-active,32.my-fade-leave-active {33transition: opacity 0.3s ease;34}3536.my-fade-enter-from,37.my-fade-leave-to {38opacity: 0;39}40</style>41```4243```vue44<!-- Parent component using the transition -->45<template>46<MyFadeTransition>47<div v-if="show">This won't animate!</div>48</MyFadeTransition>49</template>5051<!--52The <div> is slotted content, so .my-fade-* classes53applied by Vue won't match the scoped CSS selectors54-->55```5657**Correct Code:**58```vue59<!-- MyFadeTransition.vue -->60<template>61<Transition name="my-fade">62<slot />63</Transition>64</template>6566<!-- GOOD: Unscoped styles apply to any element -->67<style>68.my-fade-enter-active,69.my-fade-leave-active {70transition: opacity 0.3s ease;71}7273.my-fade-enter-from,74.my-fade-leave-to {75opacity: 0;76}77</style>78```7980## Alternative: Use Unique Prefixed Class Names8182To avoid global style conflicts, use distinctive prefixes:8384```vue85<!-- FadeTransition.vue -->86<template>87<Transition name="v-fade-transition">88<slot />89</Transition>90</template>9192<style>93/* Unique prefix reduces collision risk */94.v-fade-transition-enter-active,95.v-fade-transition-leave-active {96transition: opacity 0.3s ease;97}9899.v-fade-transition-enter-from,100.v-fade-transition-leave-to {101opacity: 0;102}103</style>104```105106## Alternative: CSS Modules with :global()107108```vue109<!-- FadeTransition.vue -->110<template>111<Transition name="fade">112<slot />113</Transition>114</template>115116<style module>117/* Use :global() for transition classes */118:global(.fade-enter-active),119:global(.fade-leave-active) {120transition: opacity 0.3s ease;121}122123:global(.fade-enter-from),124:global(.fade-leave-to) {125opacity: 0;126}127</style>128```129130## Alternative: Custom Transition Classes131132Use the custom class props to apply scoped classes:133134```vue135<!-- FadeTransition.vue -->136<template>137<Transition138:enter-active-class="$style.enterActive"139:leave-active-class="$style.leaveActive"140:enter-from-class="$style.enterFrom"141:leave-to-class="$style.leaveTo"142>143<slot />144</Transition>145</template>146147<style module>148.enterActive,149.leaveActive {150transition: opacity 0.3s ease;151}152153.enterFrom,154.leaveTo {155opacity: 0;156}157</style>158```159160## Complete Reusable Transition Component Example161162```vue163<!-- transitions/SlideTransition.vue -->164<template>165<Transition166name="slide"167:mode="mode"168:appear="appear"169@before-enter="$emit('before-enter', $event)"170@enter="$emit('enter', $event)"171@after-enter="$emit('after-enter', $event)"172@before-leave="$emit('before-leave', $event)"173@leave="$emit('leave', $event)"174@after-leave="$emit('after-leave', $event)"175>176<slot />177</Transition>178</template>179180<script setup>181defineProps({182mode: {183type: String,184default: 'out-in',185validator: (v) => ['out-in', 'in-out', ''].includes(v)186},187appear: {188type: Boolean,189default: false190}191})192193defineEmits([194'before-enter', 'enter', 'after-enter',195'before-leave', 'leave', 'after-leave'196])197</script>198199<!-- Unscoped so styles apply to slotted content -->200<style>201.slide-enter-active,202.slide-leave-active {203transition: transform 0.3s ease, opacity 0.3s ease;204}205206.slide-enter-from {207opacity: 0;208transform: translateX(-20px);209}210211.slide-leave-to {212opacity: 0;213transform: translateX(20px);214}215</style>216```217218Usage:219```vue220<template>221<SlideTransition>222<div v-if="show" class="content">223This will properly animate!224</div>225</SlideTransition>226</template>227```228229## Why This Happens230231Vue's scoped styles work by adding a unique data attribute (e.g., `data-v-7ba5bd90`) to elements and selectors:232233```css234/* What you write */235.my-fade-enter-active { ... }236237/* What Vue generates (scoped) */238.my-fade-enter-active[data-v-7ba5bd90] { ... }239```240241Slotted content comes from the parent component and gets the parent's data attribute, not the transition component's attribute. So the selectors never match.242243## Reference244- [Vue.js Reusable Transitions](https://vuejs.org/guide/built-ins/transition.html#reusable-transitions)245- [Vue.js Scoped CSS](https://vuejs.org/api/sfc-css-features.html#scoped-css)246