Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/gus-16710/invitations/llms.txt

Use this file to discover all available pages before exploring further.

Every section component in an invitation uses Framer Motion to animate its content into view as the guest scrolls through the slides. Animation definitions are centralised in a single Animations.ts file per invitation, which exports named variant objects. Components import only the variants they need, keeping the motion code out of the JSX and making it easy to swap or tweak effects across the whole invitation by editing one file.

Preset Variants from Animations.ts

The file below is the real variant set used by the Daniela XV años invitation. Each export is a Framer Motion Variants object with a hidden state (before the element enters the viewport) and a visible state (the fully rendered position and opacity).
// src/app/quinces/daniela/components/Animations.ts

export const animation01 = {
  hidden: { opacity: 0, scale: 0 },
  visible: {
    opacity: 1,
    scale: 1,
    transition: { duration: 1, type: "spring" as const, stiffness: 70, delay: 0.4 },
  },
};

export const animation02 = {
  hidden: { y: 100, rotate: 180, opacity: 0 },
  visible: {
    y: 0,
    rotate: 0,
    opacity: 1,
    transition: { duration: 1 },
  },
};

export const animation03 = {
  hidden: { y: 100, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: { duration: 1, delay: 0.8 },
  },
};

export const animation04 = {
  hidden: { y: -100, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: { duration: 1, delay: 1 },
  },
};

export const animation05 = {
  hidden: { scale: 2, opacity: 0 },
  visible: {
    scale: 1,
    opacity: 1,
    transition: { duration: 2 },
  },
};

export const animation06 = {
  hidden: { opacity: 0, scale: 0 },
  visible: {
    opacity: 1,
    scale: 1,
    transition: {
      duration: 1,
      delay: 0.6,
    },
  },
};

export const animation07 = {
  hidden: { scale: 0, opacity: 0 },
  visible: {
    scale: 1,
    opacity: 1,
    transition: { duration: 1 },
  },
};

Variant Reference

VariantHidden stateVisible stateNotable transition
animation01opacity: 0, scale: 0opacity: 1, scale: 1Spring, stiffness 70, delay 0.4 s
animation02y: 100, rotate: 180, opacity: 0y: 0, rotate: 0, opacity: 1Linear 1 s — great for decorative spinners
animation03y: 100, opacity: 0y: 0, opacity: 1Ease, delay 0.8 s — slides up from below
animation04y: -100, opacity: 0y: 0, opacity: 1Ease, delay 1 s — drops down from above
animation05scale: 2, opacity: 0scale: 1, opacity: 1Slow 2 s zoom-out — ideal for background photos
animation06opacity: 0, scale: 0opacity: 1, scale: 1Ease, delay 0.6 s — scale-in with a 0.2 s later start than animation01
animation07scale: 0, opacity: 0scale: 1, opacity: 1Ease, 1 s — no delay, immediate pop

The variants / initial / whileInView Pattern

Every animated element follows the same three-prop pattern:
<motion.h1
  variants={animation01}   // the variant object from Animations.ts
  initial="hidden"         // start in the hidden state
  whileInView="visible"    // transition to visible when the element enters the viewport
>
  Content
</motion.h1>
  • variants — points to a named variant object so the transition config lives outside the JSX.
  • initial="hidden" — sets the element’s starting state before it enters the viewport.
  • whileInView="visible" — triggers the animation when the element scrolls into view; the element returns to hidden when it leaves (enabling re-animation on revisit).

Real Usage: Header.tsx

The Header component applies four different variants to its child elements, creating a sequenced entrance: the title scales in first, the photo frame spins in, then the name and date slide up from below and drop down from above respectively.
// src/app/quinces/daniela/components/Header.tsx (excerpt)
import { motion } from "framer-motion";
import { animation01, animation02, animation03, animation04 } from "./Animations";

export default function Header() {
  return (
    <section style={{ height: "100svh" }}>

      {/* Title: spring scale-in with delay 0.4 s */}
      <motion.h1
        variants={animation01}
        initial="hidden"
        whileInView="visible"
      >
        MIS <span>XV</span> AÑOS
      </motion.h1>

      {/* Portrait photo: spring scale-in */}
      <motion.div
        className="bg-[url('/img/quinces/daniela/fifteen-girl.jpg')] ..."
        variants={animation01}
        initial="hidden"
        whileInView="visible"
      />

      {/* Decorative ring: rotates 180° into position */}
      <motion.div
        className="bg-[url('/img/quinces/daniela/header-03.png')] ..."
        variants={animation02}
        initial="hidden"
        whileInView="visible"
      />

      {/* Name: slides up from y: 100, delay 0.8 s */}
      <motion.p
        variants={animation03}
        initial="hidden"
        whileInView="visible"
      >
        Daniela
      </motion.p>

      {/* Date: drops down from y: -100, delay 1 s */}
      <motion.p
        variants={animation04}
        initial="hidden"
        whileInView="visible"
      >
        DOMINGO <span>30</span> OCTUBRE
      </motion.p>

    </section>
  );
}

Staggered List Animations

For list-like sections such as Itinerary, Framer Motion’s staggerChildren orchestration is used so each list item animates in with a 0.5 s delay between items.
// src/app/quinces/sarang/components/Itinerary.tsx (excerpt)
const list = {
  visible: {
    opacity: 1,
    transition: {
      when: "beforeChildren",
      staggerChildren: 0.5,
      duration: 1,
    },
  },
  hidden: {
    opacity: 0,
    transition: { when: "afterChildren" },
  },
};

const element = {
  visible: { opacity: 1, y: 0 },
  hidden:  { opacity: 0, y: -100 },
};

// Usage inside the component:
<motion.ol initial="hidden" whileInView="visible" variants={list}>
  {itinerary.map((item, index) => (
    <motion.div key={index} variants={element}>
      {/* item content */}
    </motion.div>
  ))}
</motion.ol>

Scroll-Snap Layout

Each section root element carries style={{ height: "100svh" }}. When the invitation is rendered without Splide (e.g., a scroll-based layout), the <html> element uses CSS scroll-snap to lock the viewport to one section at a time:
// layout.tsx or page wrapper
<html style={{ scrollSnapType: "y mandatory" }}>
  <body>
    <Header />    {/* height: 100svh, scroll-snap-align: start */}
    <Ceremony />
    <Reception />
    {/* ... */}
  </body>
</html>
Each section then implicitly snaps because the browser aligns full-viewport-height children when scrollSnapType: "y mandatory" is set on the scroll container.

Smooth Scrolling with Lenis

Scroll-based invitations wrap the entire page in a SmoothScroll component that configures Lenis for buttery smooth wheel and touch scrolling.
// src/components/SmoothScroll.tsx
"use client";

import { ReactLenis } from "lenis/react";

export default function SmoothScroll({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ReactLenis
      root
      options={{
        lerp: 0.08,        // smoothness (0 = none, 1 = instant)
        smoothWheel: true, // smooth mouse-wheel scrolling
        duration: 1.2,     // scroll animation duration in seconds
      }}
    >
      {children}
    </ReactLenis>
  );
}
Wrap the root layout body with <SmoothScroll> to enable it globally for a given invitation route.
Always use whileInView rather than animate for section content. With animate, the animation fires only once on mount — which means elements on slides 2–10 are already in their visible state before the guest ever sees them. whileInView re-triggers the entrance effect each time the slide scrolls into the viewport, creating a fresh, engaging experience on every visit to that section.

Build docs developers (and LLMs) love