963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
5.9 KiB
5.9 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| State-driven Animations with CSS Transitions and Style Bindings | LOW | Combining Vue's reactive style bindings with CSS transitions creates smooth, interactive animations | best-practice |
|
State-driven Animations with CSS Transitions and Style Bindings
Impact: LOW - For responsive, interactive animations that react to user input or state changes, combine Vue's dynamic style bindings with CSS transitions. This creates smooth animations that interpolate values in real-time based on state.
Task Checklist
- Use
:stylebinding for dynamic properties that change frequently - Add CSS
transitionproperty to smoothly animate between values - Consider using
transformandopacityfor GPU-accelerated animations - For complex value interpolation, use watchers with animation libraries
Basic Pattern
<template>
<div
@mousemove="onMousemove"
:style="{ backgroundColor: `hsl(${hue}, 80%, 50%)` }"
class="interactive-area"
>
<p>Move your mouse across this div...</p>
<p>Hue: {{ hue }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const hue = ref(0);
function onMousemove(e) {
// Map mouse X position to hue (0-360)
const rect = e.currentTarget.getBoundingClientRect();
hue.value = Math.round(((e.clientX - rect.left) / rect.width) * 360);
}
</script>
<style>
.interactive-area {
transition: background-color 0.3s ease;
height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
Common Use Cases
Following Mouse Position
<template>
<div class="container" @mousemove="onMousemove">
<div
class="follower"
:style="{
transform: `translate(${x}px, ${y}px)`,
}"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
const x = ref(0);
const y = ref(0);
function onMousemove(e) {
const rect = e.currentTarget.getBoundingClientRect();
x.value = e.clientX - rect.left;
y.value = e.clientY - rect.top;
}
</script>
<style>
.container {
position: relative;
height: 300px;
}
.follower {
position: absolute;
width: 20px;
height: 20px;
background: blue;
border-radius: 50%;
/* Smooth following with transition */
transition: transform 0.1s ease-out;
/* Prevent the follower from triggering mousemove */
pointer-events: none;
}
</style>
Progress Animation
<template>
<div class="progress-container">
<div class="progress-bar" :style="{ width: `${progress}%` }" />
</div>
<input type="range" v-model.number="progress" min="0" max="100" />
</template>
<script setup>
import { ref } from 'vue';
const progress = ref(0);
</script>
<style>
.progress-container {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4caf50, #8bc34a);
transition: width 0.3s ease;
}
</style>
Scroll-based Animation
<template>
<div
class="hero"
:style="{
opacity: heroOpacity,
transform: `translateY(${scrollOffset}px)`,
}"
>
<h1>Scroll Down</h1>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
const scrollY = ref(0);
const heroOpacity = computed(() => {
return Math.max(0, 1 - scrollY.value / 300);
});
const scrollOffset = computed(() => {
return scrollY.value * 0.5; // Parallax effect
});
function handleScroll() {
scrollY.value = window.scrollY;
}
onMounted(() => {
window.addEventListener('scroll', handleScroll, { passive: true });
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
});
</script>
<style>
.hero {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
/* Note: No transition for scroll-based animations - they should be instant */
}
</style>
Color Theme Transition
<template>
<div class="app" :style="themeStyles">
<button @click="toggleTheme">Toggle Theme</button>
<p>Current theme: {{ isDark ? 'Dark' : 'Light' }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const isDark = ref(false);
const themeStyles = computed(() => ({
'--bg-color': isDark.value ? '#1a1a1a' : '#ffffff',
'--text-color': isDark.value ? '#ffffff' : '#1a1a1a',
backgroundColor: 'var(--bg-color)',
color: 'var(--text-color)',
}));
function toggleTheme() {
isDark.value = !isDark.value;
}
</script>
<style>
.app {
min-height: 100vh;
transition:
background-color 0.5s ease,
color 0.5s ease;
}
</style>
Advanced: Numerical Tweening with Watchers
For smooth number animations (counters, stats), use watchers with animation libraries:
<template>
<div>
<input v-model.number="targetNumber" type="number" />
<p class="counter">{{ displayNumber.toFixed(0) }}</p>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue';
import gsap from 'gsap';
const targetNumber = ref(0);
const tweened = reactive({ value: 0 });
// Computed for display
const displayNumber = computed(() => tweened.value);
watch(targetNumber, (newValue) => {
gsap.to(tweened, {
duration: 0.5,
value: Number(newValue) || 0,
ease: 'power2.out',
});
});
</script>
Performance Considerations
<style>
/* GOOD: GPU-accelerated properties */
.element {
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
/* AVOID: Properties that trigger layout recalculation */
.element {
transition:
width 0.3s ease,
height 0.3s ease,
margin 0.3s ease;
}
/* For high-frequency updates, consider will-change */
.frequently-animated {
will-change: transform;
}
</style>