A feature section made of three rows. Each row pairs a block of text (eyebrow, heading, paragraph, three ticked points and a link) with an illustration, and the side the illustration sits on swaps from row to row. The three illustrations are drawn with plain Tailwind shapes: a weekly timetable, a membership card and a short message thread, each on its own tinted panel. On phones every row stacks with the text first and the illustration under it.
const rows = [
{
eyebrow: "Timetable",
title: "A week your members can read at a glance",
text: "Classes, teachers and rooms sit on one timetable. Drag a session to move it and everyone booked is told straight away.",
points: ["Repeating classes with holiday breaks", "Waiting lists that fill spare places", "Room clashes flagged before you save"],
link: "See the timetable",
},
{
eyebrow: "Payments",
title: "Passes and memberships that renew themselves",
text: "Sell single classes, ten-class passes and monthly memberships from the same checkout. Renewals and failed cards are handled for you.",
points: ["Card, wallet and bank payments", "Pause or freeze a membership in one tap", "Payouts to your account every weekday"],
link: "See payment options",
},
{
eyebrow: "Reminders",
title: "Fewer empty mats on a Monday morning",
text: "Members get a friendly nudge the evening before and can give up their place with one reply, so the waiting list moves up in time.",
points: ["Text and email, in your studio's voice", "Late-cancel rules you set once", "A weekly summary of who has gone quiet"],
link: "See how reminders work",
},
];
const check = (
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="mt-0.5 h-5 w-5 flex-none text-violet-600">
<path d="m5 10.5 3.2 3.2L15 6.8" />
</svg>
);
function Timetable() {
const days = ["Mon", "Tue", "Wed", "Thu", "Fri"];
const blocks = [
["bg-violet-600 text-white", "Flow", "7:00", "bg-white text-slate-700", "Core", "12:30"],
["bg-white text-slate-700", "Barre", "9:30", "bg-amber-300 text-amber-950", "Yin", "18:00"],
["bg-violet-200 text-violet-950", "Flow", "7:00", "bg-white text-slate-700", "Tone", "17:15"],
["bg-white text-slate-700", "Core", "10:00", "bg-violet-600 text-white", "Power", "19:00"],
["bg-amber-300 text-amber-950", "Yin", "8:15", "bg-violet-200 text-violet-950", "Flow", "16:30"],
];
return (
<div className="rounded-3xl bg-violet-50 p-5 sm:p-8">
<div className="rounded-2xl bg-white p-4 shadow-sm ring-1 ring-violet-900/5 sm:p-5">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-slate-900">Week of 14 April</p>
<span className="rounded-full bg-violet-50 px-2.5 py-1 text-xs font-medium text-violet-700">32 classes</span>
</div>
<div className="mt-4 grid grid-cols-5 gap-1.5 sm:gap-2">
{days.map((d, i) => (
<div key={d} className="space-y-1.5 sm:space-y-2">
<p className="text-center text-xs font-medium text-slate-500">{d}</p>
{[0, 3].map((o) => (
<div key={o} className={`rounded-lg px-1.5 py-2.5 ring-1 ring-inset ring-slate-900/5 sm:px-2.5 sm:py-3 ${blocks[i][o]}`}>
<p className="truncate text-xs font-semibold">{blocks[i][o + 1]}</p>
<p className="mt-0.5 text-xs opacity-75">{blocks[i][o + 2]}</p>
</div>
))}
</div>
))}
</div>
</div>
</div>
);
}
function Passes() {
return (
<div className="rounded-3xl bg-amber-50 p-5 sm:p-8">
<div className="mx-auto max-w-sm">
<div className="rounded-2xl bg-slate-900 p-5 text-white shadow-lg shadow-amber-900/10">
<div className="flex items-start justify-between">
<div>
<p className="text-xs font-medium text-slate-300">Monthly membership</p>
<p className="mt-1 text-lg font-semibold">Unlimited classes</p>
</div>
<span className="rounded-full bg-emerald-400 px-2.5 py-1 text-xs font-semibold text-emerald-950">Active</span>
</div>
<div className="mt-8 flex items-end justify-between">
<p className="text-sm text-slate-300">Priya Raman</p>
<p className="text-2xl font-semibold tracking-tight">$89<span className="text-sm font-normal text-slate-300"> / mo</span></p>
</div>
</div>
<div className="mx-3 rounded-b-2xl bg-white px-5 py-4 shadow-sm ring-1 ring-amber-900/5">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600">Renews 1 May</span>
<span className="font-medium text-slate-900">Card ending 4418</span>
</div>
<div className="mt-3 flex items-center justify-between border-t border-slate-100 pt-3 text-sm">
<span className="text-slate-600">Ten-class pass</span>
<span className="flex gap-1">
{[1, 1, 1, 1, 1, 1, 0, 0, 0, 0].map((on, i) => (
<span key={i} className={`h-2 w-2 rounded-full ${on ? "bg-amber-500" : "bg-slate-200"}`} />
))}
</span>
</div>
</div>
</div>
</div>
);
}
function Reminders() {
return (
<div className="rounded-3xl bg-sky-50 p-5 sm:p-8">
<div className="mx-auto max-w-sm space-y-3">
<p className="text-center text-xs font-medium text-slate-500">Sunday 18:30</p>
<div className="max-w-[85%] rounded-2xl rounded-bl-md bg-white px-4 py-3 text-sm leading-relaxed text-slate-700 shadow-sm ring-1 ring-sky-900/5">
Hi Tom, you are booked for <span className="font-semibold text-slate-900">Morning Flow</span> tomorrow at 7:00 with Aiko. Reply C if you cannot make it.
</div>
<div className="ml-auto w-fit rounded-2xl rounded-br-md bg-sky-600 px-4 py-3 text-sm font-medium text-white">C</div>
<div className="max-w-[85%] rounded-2xl rounded-bl-md bg-white px-4 py-3 text-sm leading-relaxed text-slate-700 shadow-sm ring-1 ring-sky-900/5">
No problem, your place is released and the class is back on your pass.
</div>
<div className="flex items-center gap-3 rounded-2xl border border-dashed border-sky-300 px-4 py-3">
<span className="flex h-8 w-8 flex-none items-center justify-center rounded-full bg-sky-200 text-xs font-semibold text-sky-900">LM</span>
<p className="text-sm text-slate-700"><span className="font-semibold text-slate-900">Lena Moretti</span> moved up from the waiting list</p>
</div>
</div>
</div>
);
}
const art = [<Timetable key="t" />, <Passes key="p" />, <Reminders key="r" />];
export default function AlternatingFeatureRowsWithIllustrations() {
return (
<section className="bg-white px-6 py-16 text-slate-900 sm:py-20">
<div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">Run the studio, not the spreadsheet</h2>
<p className="mt-4 text-lg leading-relaxed text-slate-600">Booking, payments and reminders for yoga, pilates and dance studios, in one place your members will actually use.</p>
</div>
<div className="mt-16 space-y-16 lg:mt-20 lg:space-y-20">
{rows.map((r, i) => (
<div key={r.title} className="grid items-center gap-10 lg:grid-cols-2 lg:gap-20">
<div className={i % 2 === 1 ? "lg:order-last" : ""}>
<p className="text-sm font-semibold text-violet-700">{r.eyebrow}</p>
<h3 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">{r.title}</h3>
<p className="mt-4 leading-relaxed text-slate-600">{r.text}</p>
<ul className="mt-6 space-y-3 text-sm text-slate-700">
{r.points.map((p) => (
<li key={p} className="flex items-start gap-3">{check}{p}</li>
))}
</ul>
<a href="#" className="mt-6 inline-flex min-h-[40px] items-center gap-1.5 rounded-md text-sm font-semibold text-violet-700 transition hover:text-violet-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-600 focus-visible:ring-offset-4">
{r.link}
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-4 w-4"><path d="M4.5 10h11m-4-4.5L16 10l-4.5 4.5" /></svg>
</a>
</div>
<div aria-hidden="true">{art[i]}</div>
</div>
))}
</div>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/alternating-feature-rows-with-illustrations.json
Put the text and the image in a two-column grid and add `lg:order-last` to the text column on every second row. Below the `lg` breakpoint the order class does nothing, so the columns stack in source order.
Text first works best for most pages, because the heading explains what the picture shows. If you prefer the picture first, add `order-first lg:order-none` to the illustration wrapper.
Yes. Keep the tinted rounded panel and place the screenshot inside it with `rounded-2xl shadow-sm`. Write alt text that says what the screenshot shows and remove `aria-hidden` from the wrapper.
Three or four is comfortable. Past that, visitors stop reading each one, and a grid of smaller features under the rows usually works better for the rest.
No. They are built from `div` elements, text and Tailwind colour classes, so there is nothing to download and they stay sharp at every size.
Related: Three Column Feature Grid with Icons, Bento Grid with Six Feature Tiles, Numbered How It Works Steps in a Row, Split Hero with Email Capture.
More feature sections or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.