Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.2 KiB
5.2 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | |||||
|---|---|---|---|---|---|---|---|---|---|
| Avoid Scoped Styles in Reusable Transition Components | MEDIUM | Scoped styles in transition wrapper components won't apply to slotted content, breaking the transition animation | gotcha |
|
Avoid Scoped Styles in Reusable Transition Components
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.
Task Checklist
- In reusable transition components, use
<style>withoutscoped - Alternatively, use unique class name prefixes to avoid global conflicts
- Or use CSS modules with
:global()for transition classes - Test that transitions work when component is used in different contexts
Problematic Code:
<!-- MyFadeTransition.vue -->
<template>
<Transition name="my-fade">
<slot />
</Transition>
</template>
<!-- BAD: Scoped styles won't apply to slot content! -->
<style scoped>
.my-fade-enter-active,
.my-fade-leave-active {
transition: opacity 0.3s ease;
}
.my-fade-enter-from,
.my-fade-leave-to {
opacity: 0;
}
</style>
<!-- Parent component using the transition -->
<template>
<MyFadeTransition>
<div v-if="show">This won't animate!</div>
</MyFadeTransition>
</template>
<!--
The <div> is slotted content, so .my-fade-* classes
applied by Vue won't match the scoped CSS selectors
-->
Correct Code:
<!-- MyFadeTransition.vue -->
<template>
<Transition name="my-fade">
<slot />
</Transition>
</template>
<!-- GOOD: Unscoped styles apply to any element -->
<style>
.my-fade-enter-active,
.my-fade-leave-active {
transition: opacity 0.3s ease;
}
.my-fade-enter-from,
.my-fade-leave-to {
opacity: 0;
}
</style>
Alternative: Use Unique Prefixed Class Names
To avoid global style conflicts, use distinctive prefixes:
<!-- FadeTransition.vue -->
<template>
<Transition name="v-fade-transition">
<slot />
</Transition>
</template>
<style>
/* Unique prefix reduces collision risk */
.v-fade-transition-enter-active,
.v-fade-transition-leave-active {
transition: opacity 0.3s ease;
}
.v-fade-transition-enter-from,
.v-fade-transition-leave-to {
opacity: 0;
}
</style>
Alternative: CSS Modules with :global()
<!-- FadeTransition.vue -->
<template>
<Transition name="fade">
<slot />
</Transition>
</template>
<style module>
/* Use :global() for transition classes */
:global(.fade-enter-active),
:global(.fade-leave-active) {
transition: opacity 0.3s ease;
}
:global(.fade-enter-from),
:global(.fade-leave-to) {
opacity: 0;
}
</style>
Alternative: Custom Transition Classes
Use the custom class props to apply scoped classes:
<!-- FadeTransition.vue -->
<template>
<Transition
:enter-active-class="$style.enterActive"
:leave-active-class="$style.leaveActive"
:enter-from-class="$style.enterFrom"
:leave-to-class="$style.leaveTo"
>
<slot />
</Transition>
</template>
<style module>
.enterActive,
.leaveActive {
transition: opacity 0.3s ease;
}
.enterFrom,
.leaveTo {
opacity: 0;
}
</style>
Complete Reusable Transition Component Example
<!-- transitions/SlideTransition.vue -->
<template>
<Transition
name="slide"
:mode="mode"
:appear="appear"
@before-enter="$emit('before-enter', $event)"
@enter="$emit('enter', $event)"
@after-enter="$emit('after-enter', $event)"
@before-leave="$emit('before-leave', $event)"
@leave="$emit('leave', $event)"
@after-leave="$emit('after-leave', $event)"
>
<slot />
</Transition>
</template>
<script setup>
defineProps({
mode: {
type: String,
default: 'out-in',
validator: (v) => ['out-in', 'in-out', ''].includes(v)
},
appear: {
type: Boolean,
default: false
}
})
defineEmits([
'before-enter', 'enter', 'after-enter',
'before-leave', 'leave', 'after-leave'
])
</script>
<!-- Unscoped so styles apply to slotted content -->
<style>
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-enter-from {
opacity: 0;
transform: translateX(-20px);
}
.slide-leave-to {
opacity: 0;
transform: translateX(20px);
}
</style>
Usage:
<template>
<SlideTransition>
<div v-if="show" class="content">
This will properly animate!
</div>
</SlideTransition>
</template>
Why This Happens
Vue's scoped styles work by adding a unique data attribute (e.g., data-v-7ba5bd90) to elements and selectors:
/* What you write */
.my-fade-enter-active { ... }
/* What Vue generates (scoped) */
.my-fade-enter-active[data-v-7ba5bd90] { ... }
Slotted content comes from the parent component and gets the parent's data attribute, not the transition component's attribute. So the selectors never match.