Motion

Animation standards powered by GSAP — tokens, scroll reveals, stagger, and accessibility guidance.

Overview

Epicenter uses GSAP for choreographed motion — scroll storytelling, staggered entrances, and coordinated timelines. The standard license is free for commercial use. Motion tokens live in tokens/motion.css; the brand kit helper is in lib/gsap/brand-kit.ts.

  • CSS first — hovers, focus rings, spinners, and simple state changes use --transition-*.
  • GSAP second — scroll reveals, staggered lists, and multi-step sequences.
  • Framer Motion — existing Radiant React components; not part of this brand kit standard yet.
  • Always respect prefers-reduced-motion — content must never depend on animation to be visible.

Motion tokens

Use these CSS custom properties as the source of truth. Tokens are defined in tokens/motion.css; the GSAP helper reads durations and eases from :root at runtime.

150
--motion-duration-fast
150ms · GSAP duration: 0.15
300
--motion-duration-base
300ms · GSAP duration: 0.3
600
--motion-duration-slow
600ms · GSAP duration: 0.6
in
--motion-ease-entrance
power2.out · GSAP ease: 'power2.out'
out
--motion-ease-out
power2.out · GSAP ease: 'power2.out'
24
--motion-reveal-y
18px · GSAP y: 18y: 0
.1
--motion-stagger-base
140ms · GSAP stagger: 0.14
72
--motion-marquee-duration
72s · GSAP infinite loop per track half

Scroll reveal

Add data-motion="reveal" to elements that fade and rise into view on scroll and reverse when you scroll back up.

Live preview

Scroll down in the stage to reveal each card — scroll up to replay.

Compliance

Track deadlines and reporting in one workspace.

Governance

Board materials and minutes stay organized.

Performance

Scorecards align authorizers and schools.

Intersection Observer reveal

Lightweight scroll entrances using the native Intersection Observer API — no GSAP dependency. Add data-scroll-reveal; the helper toggles scroll-reveal--in once the element crosses the viewport threshold. Motion tokens drive the CSS transition.

Live preview

Scroll down to trigger each card — animations play once and do not scrub on scroll back.

  • Native API

    Runs off the main scroll path — efficient for simple fade-and-rise entrances.

  • Token-driven

    Uses --motion-duration-slow, --motion-reveal-y, and entrance easing from motion tokens.

  • Sequential delay

    List items can use transition-delay via --motion-stagger-base for a stepped feel.

  • Simple reveals

    Prefer GSAP scroll reveal when you need scrubbed, reversible motion tied to scroll position.

Stagger

Mark a container with data-motion="stagger" and each child with data-motion-child. Children animate in sequence on scroll and reverse when you scroll back up.

Live preview

Scroll the lifecycle steps into view — scroll up to replay the sequence.

  • Application intake

  • Document review

  • Board approval

  • Renewal cycle

Bento grid

Feature cards in a responsive bento layout — built from feature card styles. Mark the grid with data-motion="bento", each card with data-motion-bento-item, and inner copy with data-motion-bento-child. Cards scale and rise on scroll as each card enters the viewport. Reverses when you scroll back up.

Live preview

Scroll the grid into view — uses the same .card--feature patterns from the cards page.

End the paper chase.

A shared calendar of expectations. Auto-routed submissions. A paper trail by default — so audits stop being events, and start being checkboxes.

On-track
94
At-risk
4
Overdue
0
Draft
12

See every school at a glance.

Roll up performance, compliance status, and open items across your full portfolio — without switching tools or rebuilding spreadsheets.

Reviews that stay on schedule.

Coordinate site visits, document requests, and follow-ups in one workflow.

Agendas and minutes in one place.

Board packets, votes, and follow-ups on a single timeline authorizers can trust.

Scorecards that align teams.

Shared goals and transparent reporting between authorizers and schools.

Scroll slider

Pin a full-height panel and advance one card per viewport of scroll. Slide content uses data-motion-slide-item for eyebrow → title → body entrance and exit on panels after the first. The opening panel stays static; scroll drives animation from the second card onward. Mark the viewport with data-motion="scroll-slider".

Live preview

When the panel below reaches the top, keep scrolling to move through each card.

Documents

Centralize charters, policies, and board packets in one shared workspace.

Compliance

Track deadlines, filings, and audit readiness without spreadsheets.

Board governance

Agendas, minutes, and votes on a single timeline authorizers can trust.

Performance

Scorecards that align authorizers and schools around shared goals.

Transparency

Share progress with stakeholders in real time — no extra exports.

Scroll expand

Pin a stage and scrub a compact background tile into a full-screen panel. Mark the wrapper with data-motion="scroll-expand"; the tile uses .brand-kit__motion-scroll-expand-bg and copy uses data-motion-scroll-expand-content.

Live preview

When the stage below reaches the top, keep scrolling to grow the tile edge-to-edge.

Scale on scroll

A small brand tile expands into a full-screen backdrop while headline copy fades in — scrubbed to scroll position and reversible when you scroll back up.

Logo marquee

An infinite logo strip built from /public/logo-timeline. Mark the wrapper with data-motion="marquee" — GSAP loops the duplicated track slowly and continuously.

Live preview

Integration logos scroll on a seamless loop. Duration is controlled by --motion-marquee-duration.

  • Loom
  • Gmail
  • Asana
  • Microsoft Teams
  • Google Calendar
  • Google Drive
  • Basecamp
  • Discord
  • HubSpot
  • Slack
  • Adobe Creative Cloud
  • Zoom

Micro-interaction

Complements CSS hover transitions on buttons. Use data-motion="micro-press" for press feedback. Loading morph runs on all brand kit buttons by default — click any button to preview.

Press feedback

Loading morph

Same behavior as every brand kit button — click to morph into a spinner-only loading state. Use startButtonLoading() and stopButtonLoading() in app code.

Morph origin (wrapper alignment)

Shrink anchor follows the nearest flex parent justify-content — start pins left, center pins middle, end pins right. Override with data-loading-morph-origin="left|center|right" on the button or wrapper.

Start

Center

End

Accessibility

  • When prefers-reduced-motion: reduce is set, initBrandKitMotion() skips all animations and marks elements complete immediately.
  • CSS in motion-demo.css ensures content is visible without JavaScript or with reduced motion.
  • Do not convey essential information through motion alone — pair with text, icons, or layout.
  • Scroll sliders fall back to native horizontal scrolling when prefers-reduced-motion is enabled.
  • Scroll expand falls back to a static full-screen panel when prefers-reduced-motion is enabled.
  • Logo marquees stop animating and show a static, scrollable row when reduced motion is preferred.
  • For loading states, use aria-busy and visible labels (see the buttons page).

Usage

Import and call the helpers from an Astro page script block:

---
// page.astro
---
<section>
  <article data-motion="reveal">...</article>
  <article data-scroll-reveal>...</article>
  <ul data-motion="stagger">
    <li data-motion-child>...</li>
  </ul>
  <div data-motion="scroll-slider" class="brand-kit__motion-scroll-slider">
    <div class="brand-kit__motion-scroll-slider-track">...</div>
  </div>
  <div data-motion="scroll-expand" class="brand-kit__motion-scroll-expand">
    <div class="brand-kit__motion-scroll-expand-pin">
      <div class="brand-kit__motion-scroll-expand-bg" aria-hidden="true"></div>
      <div data-motion-scroll-expand-content>...</div>
    </div>
  </div>
  <div class="brand-kit__bento" data-motion="bento">
    <article class="card card--feature" data-motion-bento-item>
      <div class="card__inner">
        <span class="card__icon" data-motion-bento-child>...</span>
        <h3 class="card__title" data-motion-bento-child>...</h3>
      </div>
    </article>
  </div>
  <div data-motion="marquee" class="brand-kit__motion-marquee">
    <div class="brand-kit__motion-marquee-track">...</div>
  </div>
  <button class="button button--primary">
    <span class="button__label">Save changes</span>
    <span class="button__trailing-icon material-symbols-outlined" aria-hidden="true">arrow_forward_ios</span>
    <span class="button__spinner material-symbols-outlined" aria-hidden="true">donut_large</span>
  </button>
</section>

<script>
  import { startButtonLoading, stopButtonLoading } from '../../../lib/gsap/brand-kit'
  import { initScrollReveal } from '../../../lib/brand-kit/scroll-reveal'
  // Loading morph + GSAP motion from BrandKitLayout.astro
  initScrollReveal()
  // startButtonLoading(button) / stopButtonLoading(button) for app code
</script>

For Astro islands or SPA navigation, call ScrollTrigger.getAll().forEach((t) => t.kill()) before re-initializing to avoid duplicate triggers.

Back to site