Feature Split
Two-column feature block with checklist and a visual that slides in.
Collaboration
Review work where it lives
Leave comments directly on the page, mention teammates and resolve feedback without a single screenshot.
- Pin comments to any element
- Threaded replies and mentions
- Resolve with one click
Maya: Can we make this heading bolder?
components/ui/feature-split.tsx
"use client";
import { motion } from "motion/react";
import { Check } from "lucide-react";
import type { ReactNode } from "react";
type FeatureSplitProps = {
eyebrow: string;
title: string;
text: string;
points: string[];
visual: ReactNode;
reverse?: boolean;
};
export function FeatureSplit({ eyebrow, title, text, points, visual, reverse = false }: FeatureSplitProps) {
return (
<div className="grid items-center gap-10 md:grid-cols-2 md:gap-16">
<motion.div
initial={{ opacity: 0, x: reverse ? 30 : -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}
className={reverse ? "md:order-2" : ""}
>
<p className="text-sm font-medium text-blue-600">{eyebrow}</p>
<h3 className="mt-2 text-3xl font-semibold tracking-tight text-zinc-900">{title}</h3>
<p className="mt-4 leading-relaxed text-zinc-500">{text}</p>
<ul className="mt-6 space-y-3">
{points.map((p, i) => (
<motion.li
key={p}
initial={{ opacity: 0, y: 8 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 + i * 0.08 }}
className="flex items-center gap-3 text-sm text-zinc-700"
>
<span className="grid size-5 shrink-0 place-items-center rounded-full bg-blue-600 text-white">
<Check className="size-3" />
</span>
{p}
</motion.li>
))}
</ul>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
>
{visual}
</motion.div>
</div>
);
}
export default function Demo() {
return (
<div className="w-full max-w-5xl">
<FeatureSplit
eyebrow="Collaboration"
title="Review work where it lives"
text="Leave comments directly on the page, mention teammates and resolve feedback without a single screenshot."
points={["Pin comments to any element", "Threaded replies and mentions", "Resolve with one click"]}
visual={
<div className="relative aspect-[4/3] overflow-hidden rounded-3xl bg-gradient-to-br from-blue-50 to-violet-100 p-8">
<div className="h-full rounded-2xl bg-white p-5 shadow-xl">
<div className="h-3 w-24 rounded bg-zinc-200" />
<div className="mt-4 h-24 rounded-xl bg-zinc-100" />
<div className="mt-3 h-3 w-3/4 rounded bg-zinc-100" />
</div>
<motion.div
animate={{ y: [0, -6, 0] }}
transition={{ duration: 3, repeat: Infinity }}
className="absolute right-6 top-16 max-w-[180px] rounded-xl bg-zinc-900 p-3 text-xs text-white shadow-lg"
>
<span className="font-medium">Maya:</span> Can we make this heading bolder?
</motion.div>
</div>
}
/>
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/feature-split.tsxcomponents/ui/feature-split.tsx"use client"; import { motion } from "motion/react"; import { Check } from "lucide-react"; import type { ReactNode } from "react"; type FeatureSplitProps = { eyebrow: string; title: string; text: string; points: string[]; visual: ReactNode; reverse?: boolean; }; export function FeatureSplit({ eyebrow, title, text, points, visual, reverse = false }: FeatureSplitProps) { return ( <div className="grid items-center gap-10 md:grid-cols-2 md:gap-16"> <motion.div initial={{ opacity: 0, x: reverse ? 30 : -30 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true, amount: 0.4 }} transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }} className={reverse ? "md:order-2" : ""} > <p className="text-sm font-medium text-blue-600">{eyebrow}</p> <h3 className="mt-2 text-3xl font-semibold tracking-tight text-zinc-900">{title}</h3> <p className="mt-4 leading-relaxed text-zinc-500">{text}</p> <ul className="mt-6 space-y-3"> {points.map((p, i) => ( <motion.li key={p} initial={{ opacity: 0, y: 8 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: 0.2 + i * 0.08 }} className="flex items-center gap-3 text-sm text-zinc-700" > <span className="grid size-5 shrink-0 place-items-center rounded-full bg-blue-600 text-white"> <Check className="size-3" /> </span> {p} </motion.li> ))} </ul> </motion.div> <motion.div initial={{ opacity: 0, scale: 0.95 }} whileInView={{ opacity: 1, scale: 1 }} viewport={{ once: true, amount: 0.4 }} transition={{ duration: 0.8, ease: [0.22, 1, 0.36, 1] }} > {visual} </motion.div> </div> ); } export default function Demo() { return ( <div className="w-full max-w-5xl"> <FeatureSplit eyebrow="Collaboration" title="Review work where it lives" text="Leave comments directly on the page, mention teammates and resolve feedback without a single screenshot." points={["Pin comments to any element", "Threaded replies and mentions", "Resolve with one click"]} visual={ <div className="relative aspect-[4/3] overflow-hidden rounded-3xl bg-gradient-to-br from-blue-50 to-violet-100 p-8"> <div className="h-full rounded-2xl bg-white p-5 shadow-xl"> <div className="h-3 w-24 rounded bg-zinc-200" /> <div className="mt-4 h-24 rounded-xl bg-zinc-100" /> <div className="mt-3 h-3 w-3/4 rounded bg-zinc-100" /> </div> <motion.div animate={{ y: [0, -6, 0] }} transition={{ duration: 3, repeat: Infinity }} className="absolute right-6 top-16 max-w-[180px] rounded-xl bg-zinc-900 p-3 text-xs text-white shadow-lg" > <span className="font-medium">Maya:</span> Can we make this heading bolder? </motion.div> </div> } /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.