SectionsAdvancedApril 27, 2026

Slide Pillars

A scroll-driven services section with cycling headings, animated counter with progress bar, and staggered service lists that slide in and out per pillar. Pinned full-viewport layout with left info column and right service list column.

View Full Demo →

Research

Strategy

Design

Development

Content

00
1
2
3
4
5
005

We build beautiful digital products and experiences for brands, organisations and agencies. We're obsessed with technology and harness our skills in research and strategy, branding, UX/UI, and development to create impactful web experiences that drive value for our clients and their customers.

Our Services

Customer Research

Trends Analysis

Competitor Review

Best Practice Review

Usability Research & Testing

Market Research

Analytics Reports

Product Ideation

Brand Positioning & Architecture

Brand Naming & Strategy

Target Audience Discovery

Customer Journey Mapping

Feature Definition

Information Architecture

Usability Audit & Review

Post-Launch Strategy

Art Direction

Brand Identity Design

Design Systems

Graphic Design

Wireframing

User Interface Design

User Experience Design

Interaction Design

3D Design

Motion Design

Digital Product Design

Prototyping

Packaging

Quality Assurance

Technical Strategy

Technical Consulting

CMS Implementation

React / Next.js Development

WebGL / 3D Development

Shopify Development

WordPress Development

Webflow Development

Cross-browser Testing

Cross-device Testing

SEO & Performance Optimisation

Quality Assurance

Copywriting

SEO Copy Analysis & Refinement

Thematic Keyword Research

Social Media Design

Content Management

demo.jsx
import SlidePillars from "./index.jsx";
import { slidePillars } from "../demo-data.js";

export default function SlidePillarsDemo() {
  return <SlidePillars {...slidePillars} />;
}
index.jsx
"use client";

import { useEffect, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import styles from "./styles.module.css";

gsap.registerPlugin(ScrollTrigger);

export default function SlidePillars({
  pillars = [],
  description = "",
  ctaLabel = "Our Services",
  ctaHref = "/services",
}) {
  const sectionRef = useRef(null);
  const headingsRef = useRef(null);
  const numsRef = useRef(null);
  const progressRef = useRef(null);
  const listsRef = useRef(null);

  const total = pillars.length;
  const totalFormatted = String(total).padStart(3, "0");

  useEffect(() => {
    if (!pillars.length) return;

    const ctx = gsap.context(() => {
      const steps = pillars.length;
      const headingsEl = headingsRef.current;
      const numsEl = numsRef.current;
      const progressEl = progressRef.current;
      const section = sectionRef.current;

      if (!headingsEl || !numsEl || !progressEl) return;

      // Main scroll-driven timeline
      const tl = gsap.timeline({
        scrollTrigger: {
          trigger: section,
          start: "top top",
          end: `+=${steps * 100}%`,
          pin: true,
          scrub: 0.5,
        },
      });

      // Headings cycle
      tl.to(headingsEl, {
        yPercent: -((steps - 1) / steps) * 100,
        ease: "none",
      }, 0);

      // Counter numbers cycle
      tl.to(numsEl, {
        yPercent: -((steps - 1) / steps) * 100,
        ease: "none",
      }, 0);

      // Progress bar
      tl.fromTo(progressEl, { xPercent: -100 }, { xPercent: 0, ease: "none" }, 0);

      // Right column vertical scroll, per-segment
      const listWraps = section.querySelectorAll(`.${styles.listWrap}`);
      const listsContainer = listsRef.current;
      if (listsContainer && listWraps.length) {
        const seg = 1 / steps;
        let cumulativeY = 0;

        for (let i = 0; i < steps - 1; i++) {
          const targetY = cumulativeY + listWraps[i].offsetHeight;
          tl.to(listsContainer, { y: -targetY, ease: "none" }, i * seg);
          cumulativeY = targetY;
        }
      }
    }, sectionRef);

    return () => ctx.revert();
  }, [pillars]);

  return (
    <section ref={sectionRef} className={styles.section}>
      <div className={styles.inner}>
        {/* Left column */}
        <div className={styles.column}>
          <header className={styles.headingsWrap}>
            <div ref={headingsRef} className={styles.headings}>
              {pillars.map((p, i) => (
                <h2 key={i} className={styles.heading}>
                  {p.title}
                </h2>
              ))}
            </div>
          </header>

          <div className={styles.infoWrap}>
            <div className={styles.counterWrap}>
              <div className={styles.counter}>
                <div className={styles.numWrap}>
                  <div>00</div>
                  <div ref={numsRef} className={styles.nums}>
                    {pillars.map((_, i) => (
                      <div key={i} className={styles.num}>
                        {i + 1}
                      </div>
                    ))}
                  </div>
                </div>
                <div className={styles.progressWrap}>
                  <div ref={progressRef} className={styles.progress} />
                </div>
              </div>
              <div className={styles.countTotal}>{totalFormatted}</div>
            </div>

            <p className={styles.paragraph}>{description}</p>

            <a className={styles.button} href={ctaHref}>
              <span className={styles.buttonText}>{ctaLabel}</span>
            </a>
          </div>
        </div>

        {/* Right column */}
        <div className={`${styles.column} ${styles.columnRight}`}>
          <div ref={listsRef} className={styles.listsContainer}>
          {pillars.map((p, i) => (
            <div key={i} className={styles.listWrap}>
              {p.services.map((service, j) => (
                <h3 key={j} className={styles.listItem}>
                  {service}
                </h3>
              ))}
              <div className={styles.separatorWrap}>
                <div className={styles.separator} />
              </div>
            </div>
          ))}
          </div>
        </div>
      </div>
    </section>
  );
}
styles.module.css
.section {
  position: relative;
  padding: 4vw 0;
  background-color: #bcbcb4;
  color: #2a2a2a;
}

.inner {
  display: flex;
  gap: 0;
  padding: 0 2.5vw;
  height: calc(100dvh - 8vw);
  align-items: stretch;
}

/* ---- Columns ---- */
.column {
  display: flex;
  flex-direction: column;
}

.column:first-child {
  flex: 0 0 48%;
  max-width: 48%;
  justify-content: space-between;
  padding: 2vw 2vw 2vw 0;
}

.columnRight {
  flex: 1;
  position: relative;
  overflow: hidden;
  justify-content: center;
}

/* ---- Headings ---- */
.headingsWrap {
  height: clamp(3.5rem, 8vw, 7rem);
  overflow: hidden;
  position: relative;
}

.headings {
  display: flex;
  flex-direction: column;
}

.heading {
  font-size: clamp(3rem, 7vw, 6rem);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  height: clamp(3.5rem, 8vw, 7rem);
  display: flex;
  align-items: center;
}

/* ---- Info wrap ---- */
.infoWrap {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  max-width: 500px;
}

/* ---- Counter ---- */
.counterWrap {
  display: flex;
  align-items: center;
  gap: 0;
}

.counter {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex: 1;
}

.numWrap {
  display: flex;
  align-items: baseline;
  gap: 0;
  font-size: clamp(0.85rem, 1.1vw, 1rem);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  height: 1.2em;
}

.nums {
  display: flex;
  flex-direction: column;
}

.num {
  height: 1.2em;
  display: flex;
  align-items: center;
}

.progressWrap {
  flex: 1;
  height: 1px;
  background: rgba(0, 0, 0, 0.15);
  overflow: hidden;
}

.progress {
  width: 100%;
  height: 100%;
  background: #2a2a2a;
  transform: translateX(-100%);
}

.countTotal {
  font-size: clamp(0.85rem, 1.1vw, 1rem);
  font-variant-numeric: tabular-nums;
}

/* ---- Paragraph ---- */
.paragraph {
  font-size: clamp(0.85rem, 1vw, 0.95rem);
  line-height: 1.55;
}

/* ---- Button ---- */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.75em 1.8em;
  background: #2a2a2a;
  border: none;
  border-radius: 999px;
  font-size: clamp(0.75rem, 0.9vw, 0.875rem);
  text-decoration: none;
  color: #fff;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  width: fit-content;
  transition: opacity 0.3s ease;
}

.button:hover {
  opacity: 0.8;
}

.buttonText {
  position: relative;
}

/* ---- Right column — lists ---- */
.listsContainer {
  display: flex;
  flex-direction: column;
  will-change: transform;
}

.listWrap {
  display: flex;
  flex-direction: column;
  padding: 0 2vw;
}

.listItem {
  font-size: clamp(1.1rem, 2.2vw, 1.75rem);
  font-weight: 400;
  line-height: 1;
  padding: 0.5em 0;
  text-transform: uppercase;
  will-change: transform;
}

.separatorWrap {
  padding-top: 1em;
  overflow: hidden;
}

.separator {
  height: 1px;
  background: rgba(0, 0, 0, 0.2);
  width: 100%;
  will-change: transform;
}

/* ---- Mobile ---- */
@media (max-width: 768px) {
  .inner {
    flex-direction: column;
    padding: 0 1.25rem;
    height: calc(100dvh - 8vw);
  }

  .column:first-child {
    flex: none;
    max-width: 100%;
    padding: 1.5rem 0 0;
  }

  .columnRight {
    flex: 1;
    min-height: 40vh;
  }

  .headingsWrap {
    height: clamp(2.5rem, 10vw, 4rem);
  }

  .heading {
    font-size: clamp(2rem, 9vw, 3.5rem);
    height: clamp(2.5rem, 10vw, 4rem);
  }

  .listItem {
    font-size: clamp(0.9rem, 4vw, 1.25rem);
  }

  .infoWrap {
    gap: 1rem;
    padding-bottom: 1.5rem;
  }
}
  • gsap

May 11, 2026

SECTIONS

Dual Push Cards

Two-up CTA card section with scroll-driven parallax on each card's background image. Cards scale down and drift vertically as you scroll past. Glassmorphic blur buttons at bottom-left. Stacks on mobile, side-by-side grid on desktop.

May 4, 2026

SECTIONS

Portfolio Grid

A responsive portfolio showcase section with a header tagline, blinking cursor counters, a 2-up/4-col project card grid with hover-zoom images and data-label metadata, plus a full-width CTA button. Scroll-triggered fade and move-up animations via GSAP.

May 1, 2026

SECTIONS

Logo Wall Cycle

A responsive logo grid that cycles through brand logos with smooth GSAP-powered swap animations. Shows 8 logos on desktop and 6 on tablet, shuffling hidden logos into view on a timed loop. Pauses when out of viewport or tab is hidden.