Services Accordion
A services section with a large SplitText heading reveal, description, and expandable accordion rows. Each row fills with a black panel on hover, inverting text to white. Expanded state reveals a description and a responsive grid of sub-services.
View Full Demo →Preview
What We Do
We bring together the right human expertise with the best use of technology to help define, design and accelerate delivery of more valuable, personal interactions between people and brands across their full ecosystem.
Creative Direction
Codifying the creative language of your brand across every touchpoint — from narrative systems to motion — to deliver clarity, consistency and impact at scale.
Creative Platform
Defining the core idea, tone and territories that guide all expressions.
Visual Language
Typography, color, grids, art direction and imagery systems for a cohesive look & feel.
Narrative Systems
Messaging architecture, voice & tone, taglines and story frameworks.
Campaign Concepts
Big ideas, key visuals and toolkits engineered for multi-channel rollout.
Editorial Design
Swiss-inspired systems for long-form, microsites and content hubs.
Motion & Film
Motion language, transitions, type in motion, and direction for live-action & CG.
Design Ops
Components, specs and QA to ensure consistency from Figma to code.
Brand Toolkits
Practical libraries, templates and guidance for teams to ship faster.
AI-Aided Workflows
Prototyping, exploration and asset generation to accelerate creative delivery.
Film & Motion Design
Codifying the creative language of your brand across every touchpoint — from narrative systems to motion — to deliver clarity, consistency and impact at scale.
Creative Platform
Defining the core idea, tone and territories that guide all expressions.
Visual Language
Typography, color, grids, art direction and imagery systems for a cohesive look & feel.
Narrative Systems
Messaging architecture, voice & tone, taglines and story frameworks.
Campaign Concepts
Big ideas, key visuals and toolkits engineered for multi-channel rollout.
Editorial Design
Swiss-inspired systems for long-form, microsites and content hubs.
Motion & Film
Motion language, transitions, type in motion, and direction for live-action & CG.
Design Ops
Components, specs and QA to ensure consistency from Figma to code.
Brand Toolkits
Practical libraries, templates and guidance for teams to ship faster.
AI-Aided Workflows
Prototyping, exploration and asset generation to accelerate creative delivery.
Brand Strategy
Positioning, architecture and go-to-market plans that give creative work a strategic foundation.
Creative Platform
Defining the core idea, tone and territories that guide all expressions.
Visual Language
Typography, color, grids, art direction and imagery systems for a cohesive look & feel.
Narrative Systems
Messaging architecture, voice & tone, taglines and story frameworks.
Campaign Concepts
Big ideas, key visuals and toolkits engineered for multi-channel rollout.
Editorial Design
Swiss-inspired systems for long-form, microsites and content hubs.
Motion & Film
Motion language, transitions, type in motion, and direction for live-action & CG.
Source
import ServicesAccordion from "./index.jsx";
import { servicesAccordion } from "../demo-data.js";
export default function ServicesAccordionDemo() {
return <ServicesAccordion {...servicesAccordion} />;
}
"use client";
import { useEffect, useRef, useState } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import styles from "./styles.module.css";
gsap.registerPlugin(ScrollTrigger, SplitText);
function AccordionItem({ item }) {
const [open, setOpen] = useState(false);
const contentRef = useRef(null);
return (
<div
className={`${styles.item} ${open ? styles.open : ""}`}
aria-expanded={open}
>
{/* Black fill panel */}
<div className={styles.fillPanel} />
{/* Header row */}
<div className={styles.itemHeader} onClick={() => setOpen((o) => !o)}>
<div className={styles.itemTitle}>
<p>{item.title}</p>
</div>
<div className={styles.itemToggle}>+</div>
</div>
{/* Expandable content */}
<div
className={styles.itemContent}
ref={contentRef}
style={{ maxHeight: open ? `${contentRef.current?.scrollHeight}px` : "0px" }}
>
<div className={styles.contentInner}>
{item.description && (
<p className={styles.contentDesc}>{item.description}</p>
)}
{item.services?.length > 0 && (
<ul className={styles.serviceGrid}>
{item.services.map((service, i) => (
<li key={i} className={styles.serviceItem}>
<p className={styles.serviceTitle}>{service.title}</p>
<p className={styles.serviceDesc}>{service.description}</p>
</li>
))}
</ul>
)}
</div>
</div>
</div>
);
}
export default function ServicesAccordion({
heading = "What We Do",
description = "",
items = [],
}) {
const sectionRef = useRef(null);
const headingRef = useRef(null);
const descRef = useRef(null);
useEffect(() => {
const ctx = gsap.context(() => {
const h2 = headingRef.current;
if (h2) {
SplitText.create(h2, {
type: "chars",
charsClass: styles.charChild,
mask: "chars",
});
const chars = h2.querySelectorAll(`.${styles.charChild}`);
gsap.from(chars, {
yPercent: 100,
opacity: 0,
duration: 0.8,
stagger: 0.03,
ease: "power4.out",
scrollTrigger: {
trigger: sectionRef.current,
start: "top 75%",
once: true,
},
});
}
if (descRef.current) {
gsap.from(descRef.current, {
y: 20,
opacity: 0,
duration: 0.8,
ease: "power3.out",
scrollTrigger: {
trigger: sectionRef.current,
start: "top 65%",
once: true,
},
});
}
}, sectionRef);
return () => ctx.revert();
}, []);
return (
<section ref={sectionRef} className={styles.section}>
<h2 ref={headingRef} className={styles.heading}>
{heading}
</h2>
{description && (
<div ref={descRef}>
<p className={styles.description}>{description}</p>
</div>
)}
<div className={styles.accordion}>
{items.map((item, i) => (
<AccordionItem key={i} item={item} />
))}
</div>
</section>
);
}
.section {
width: 100%;
min-height: 100dvh;
padding: 0 2rem;
}
/* ---- Heading ---- */
.heading {
font-size: clamp(3rem, 10vw, 9rem);
font-weight: 700;
line-height: 1;
letter-spacing: -0.03em;
color: #000;
margin-bottom: -0.2em;
padding: 0 2rem;
text-transform: uppercase;
}
.charChild {
position: relative;
display: inline-block;
}
/* ---- Description ---- */
.description {
padding: 2rem;
margin-bottom: 2rem;
max-width: 70ch;
font-size: clamp(14px, 1.2vw, 18px);
line-height: 1.45;
color: rgba(0, 0, 0, 0.8);
}
/* ---- Accordion ---- */
.accordion {
width: 100%;
}
/* ---- Item ---- */
.item {
position: relative;
overflow: hidden;
cursor: pointer;
border-top: 1px solid rgba(0, 0, 0, 0.05);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.7s ease-in-out;
}
/* ---- Black fill panel (hover) ---- */
.fillPanel {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%) scaleY(0);
width: 170%;
height: 150%;
background: #000;
transition: transform 0.7s ease-in-out;
transform-origin: top;
pointer-events: none;
z-index: 0;
}
.item:hover .fillPanel,
.item.open .fillPanel {
transform: translateX(-50%) scaleY(1);
}
/* ---- Header row ---- */
.itemHeader {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
height: 150px;
z-index: 1;
}
.itemTitle {
padding-left: 2rem;
transition: transform 0.7s ease-in-out;
}
.itemTitle p {
font-size: clamp(1.5rem, 3vw, 2.5rem);
text-transform: uppercase;
color: #000;
transition: color 0.7s ease-in-out;
}
.item:hover .itemTitle,
.item.open .itemTitle {
transform: translateX(2rem);
}
.item:hover .itemTitle p,
.item.open .itemTitle p {
color: #fff;
}
.itemToggle {
padding-right: 1rem;
font-size: 2.25rem;
color: #000;
transition: transform 0.7s ease-in-out, color 0.7s ease-in-out;
z-index: 1;
}
.item:hover .itemToggle,
.item.open .itemToggle {
transform: scale(3);
color: #fff;
}
/* ---- Expandable content ---- */
.itemContent {
position: relative;
max-height: 0;
overflow: hidden;
transition: max-height 0.7s ease-in-out;
z-index: 1;
}
.contentInner {
padding: 0 4rem 2rem;
}
.contentDesc {
max-width: 55ch;
font-size: 1rem;
line-height: 1.5;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 2.5rem;
}
/* ---- Service grid ---- */
.serviceGrid {
display: grid;
grid-template-columns: 1fr;
gap: 2.5rem 3rem;
list-style: none;
}
.serviceTitle {
font-size: 0.95rem;
font-weight: 600;
color: #fff;
margin-bottom: 0.35rem;
}
.serviceDesc {
font-size: 0.875rem;
line-height: 1.5;
color: rgba(255, 255, 255, 0.6);
}
/* ---- Responsive ---- */
@media (min-width: 768px) {
.serviceGrid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.serviceGrid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.section {
padding: 0 1rem;
}
.heading {
padding: 0 1rem;
}
.description {
padding: 1rem;
}
.itemTitle {
padding-left: 1rem;
}
.contentInner {
padding: 0 1rem 2rem;
}
}
Dependencies
gsap