shadimage
vue-bits
componentAnimatedContentsource: ok · hit

AnimatedContent

Wrapper that animates any children on scroll or mount with configurable direction, distance, duration, easing and disappear options.

npx shadcn add "https://vue-bits.dev/r/AnimatedContent.json"
free

Interaction

scroll-reveal

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
gsap@^3.13.0

Files1 · source cached 10/9/2026, 4:39:16 PM

AnimatedContent/AnimatedContent.vue
<script setup lang="ts">
import { onMounted, onUnmounted, watch, useTemplateRef } from 'vue';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

interface AnimatedContentProps {
  distance?: number;
  direction?: 'vertical' | 'horizontal';
  reverse?: boolean;
  duration?: number;
  ease?: string | ((progress: number) => number);
  initialOpacity?: number;
  animateOpacity?: boolean;
  scale?: number;
  threshold?: number;
  delay?: number;
  className?: string;
}

const props = withDefaults(defineProps<AnimatedContentProps>(), {
  distance: 100,
  direction: 'vertical',
  reverse: false,
  duration: 0.8,
  ease: 'power3.out',
  initialOpacity: 0,
  animateOpacity: true,
  scale: 1,
  threshold: 0.1,
  delay: 0,
  className: ''
});

const emit = defineEmits<{
  complete: [];
}>();

const containerRef = useTemplateRef<HTMLDivElement>('containerRef');

onMounted(() => {
  const el = containerRef.value;
  if (!el) return;

  const axis = props.direction === 'horizontal' ? 'x' : 'y';
  const offset = props.reverse ? -props.distance : props.distance;
  const startPct = (1 - props.threshold) * 100;

  gsap.set(el, {
    [axis]: offset,
    scale: props.scale,
    opacity: props.animateOpacity ? props.initialOpacity : 1
  });

  gsap.to(el, {
    [axis]: 0,
    scale: 1,
    opacity: 1,
    duration: props.duration,
    ease: props.ease,
    delay: props.delay,
    onComplete: () => emit('complete'),
    scrollTrigger: {
      trigger: el,
      start: `top ${startPct}%`,
      toggleActions: 'play none none none',
      once: true
    }
  });
});

watch(
  () => [
    props.distance,
    props.direction,
    props.reverse,
    props.duration,
    props.ease,
    props.initialOpacity,
    props.animateOpacity,
    props.scale,
    props.threshold,
    props.delay
  ],
  () => {
    const el = containerRef.value;
    if (!el) return;

    ScrollTrigger.getAll().forEach(t => t.kill());
    gsap.killTweensOf(el);

    const axis = props.direction === 'horizontal' ? 'x' : 'y';
    const offset = props.reverse ? -props.distance : props.distance;
    const startPct = (1 - props.threshold) * 100;

    gsap.set(el, {
      [axis]: offset,
      scale: props.scale,
      opacity: props.animateOpacity ? props.initialOpacity : 1
    });

    gsap.to(el, {
      [axis]: 0,
      scale: 1,
      opacity: 1,
      duration: props.duration,
      ease: props.ease,
      delay: props.delay,
      onComplete: () => emit('complete'),
      scrollTrigger: {
        trigger: el,
        start: `top ${startPct}%`,
        toggleActions: 'play none none none',
        once: true
      }
    });
  },
  { deep: true }
);

onUnmounted(() => {
  const el = containerRef.value;
  if (el) {
    ScrollTrigger.getAll().forEach(t => t.kill());
    gsap.killTweensOf(el);
  }
});
</script>

<template>
  <div ref="containerRef" :class="`animated-content ${props.className}`">
    <slot />
  </div>
</template>

<style scoped>
/* GSAP will handle all transforms and opacity */
</style>

More from vue-bits

Browse all components in this library →