Files
stack/packages/mosaic/framework/skills/vue-best-practices/reference/animation-state-driven-technique.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
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.
2026-08-19 14:37:17 -05:00

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
vue3
animation
css
transition
style-binding
state
interactive

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 :style binding for dynamic properties that change frequently
  • Add CSS transition property to smoothly animate between values
  • Consider using transform and opacity for 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>

Reference