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.
Latest creations and projects
What teams say after shipping with us — pulled from reviews left over the last twelve months.
The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.
The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.
The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.
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.
The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.
The photos were sharp, clean, and made our website look premium. They truly captured the essence of our brand.
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-driftUsage
import ColumnDrift from '@/components/tweenui/column-drift';
export default function Page() {
return <ColumnDrift />;
}Pass your own reviews. Each array is one column:
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
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | sample heading | Section heading. |
description | string | sample line | Supporting line under the heading. |
columns | ColumnDriftItem[][] | three columns | One array per column. Three is the intended shape. |
rating | ColumnDriftRating | null | sample summary | Summary card in the middle column. null hides it. |
Extends native <section> attributes (className, style, …).
ColumnDriftItem
| Field | Type | Description |
|---|---|---|
name · jobTitle | string | Reviewer identity. |
image · imageAlt | string | Avatar; imageAlt falls back to name. |
quoteBefore · quoteHighlight · quoteAfter | string | The quote, split so the middle clause can take the underline wipe. |
stars | number | Filled 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.