Blocks

Column Drift

Three columns of review cards that swing into place on scroll, then drift past each other.

A ready-made social-proof section. The outer columns swing in from the sides and the middle one lands a beat later, so the wall closes inward. After that the three columns drift at different speeds for the rest of the scroll. Cards lift on hover, the avatar flips, and the highlighted clause gets an underline wipe. GSAP + ScrollTrigger.

New tab

Latest creations and projects

What teams say after shipping with us — pulled from reviews left over the last twelve months.

Liam HarperLiam HarperDog Trainer

The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.

Maya CollinsMaya CollinsNursing Assistant

The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.

Ethan BrooksEthan BrooksPresident of Sales

The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.

+243Client avatarClient avatarClient avatar

Happy by 20k+ clients

4.8/5

Real Rating

The photos were sharp, clean, and made our website look premium.

They truly captured the essence of our brand.

Zoe MitchellZoe MitchellWeb Designer

The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.

Noah BennettNoah BennettProduct Lead

The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.

Ava SinclairAva SinclairMarketing Head

The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.

Installation

pnpm dlx shadcn@latest add @tween-ui/column-drift

Usage

app/page.tsx
app/page.tsx
import ColumnDrift from '@/components/tweenui/column-drift';
 
export default function Page() {
  return <ColumnDrift />;
}

Pass your own reviews. Each array is one column:

app/page.tsx
app/page.tsx
import ColumnDrift from '@/components/tweenui/column-drift';
 
const review = (name: string, jobTitle: string, image: string) => ({
  name,
  jobTitle,
  image,
  quoteBefore: 'The rebuild shipped in three weeks,',
  quoteHighlight: 'and conversions moved the same month.',
  quoteAfter: 'Worth every call.',
  stars: 5,
});
 
export default function Page() {
  return (
    <ColumnDrift
      title="Trusted by teams that ship"
      columns={[
        [review('Liam Harper', 'Head of Growth', '/avatars/liam.jpg')],
        [review('Ethan Brooks', 'President of Sales', '/avatars/ethan.jpg')],
        [review('Ava Sinclair', 'Marketing Head', '/avatars/ava.jpg')],
      ]}
    />
  );
}

Hide the summary card with rating={null}:

<ColumnDrift rating={null} />

Props

PropTypeDefaultDescription
titleReactNodesample headingSection heading.
descriptionstringsample lineSupporting line under the heading.
columnsColumnDriftItem[][]three columnsOne array per column. Three is the intended shape.
ratingColumnDriftRating | nullsample summarySummary card in the middle column. null hides it.

Extends native <section> attributes (className, style, …).

ColumnDriftItem

FieldTypeDescription
name · jobTitlestringReviewer identity.
image · imageAltstringAvatar; imageAlt falls back to name.
quoteBefore · quoteHighlight · quoteAfterstringThe quote, split so the middle clause can take the underline wipe.
starsnumberFilled stars out of five. Defaults to 4.

The summary card is placed inside the second column, so it only appears when you pass at least two columns.

Accessibility

Each star row carries an aria-label with its filled count, and avatars fall back to the reviewer's name for alt text. Under prefers-reduced-motion: reduce nothing animates — no swing-in, no drift, no hover lift — and every card renders at its final position.