Feature Grid
Explore project conversations, handoffs, and briefs in a selectable feature section.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/feature-grid.jsonUse it
Scroll horizontally for long lines
"use client";
import {FeatureGrid} from '@/components/jez-ui/blocks/feature-grid';
export default function Example(){
return <><FeatureGrid/></>;
}Props & types
Scroll horizontally for long lines
{ className?: string }Source
View 2 source files
registry/blocks/feature-grid.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import {
ArrowUpRight,
Check,
FileText,
MessageSquare,
GitBranch,
} from "lucide-react";
import { cn } from "../ui/utils";
const features = [
{
title: "Keep the decision with the work.",
text: "Briefs, comments, and the latest decision belong beside the task. Pick up where the conversation left off.",
icon: MessageSquare,
},
{
title: "Give every task an owner.",
text: "See who’s moving things forward, what’s waiting for review, and where someone needs a hand.",
icon: GitBranch,
},
{
title: "Leave a useful paper trail.",
text: "A short project brief gives everyone the same starting point. Keep the scope and release criteria close.",
icon: FileText,
},
];
export function FeatureGrid({ className }: { className?: string }) {
const [active, setActive] = React.useState(0);
return (
<section className={cn("border-y border-border py-12 md:py-20", className)}>
<div className="mb-10 grid gap-4 md:grid-cols-2">
<h2 className="max-w-md text-3xl leading-tight md:text-4xl">
The context is part
<br />
of the project.
</h2>
<p className="max-w-sm text-sm leading-relaxed text-muted-foreground md:justify-self-end">
Work doesn’t happen in a list alone. Give your team the decisions,
ownership, and detail they need to finish it.
</p>
</div>
<div className="grid gap-8 md:grid-cols-[.85fr_1.15fr] md:gap-16">
<div className="grid content-start">
{features.map((f, i) => (
<button
key={f.title}
type="button"
aria-pressed={active === i}
onClick={() => setActive(i)}
className={cn(
"border-b border-border py-6 text-left first:pt-0 last:border-0",
active === i
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
<span className="flex items-center gap-3">
<f.icon
size={17}
className={active === i ? "text-primary" : ""}
/>
<span className="flex-1 text-base font-medium">{f.title}</span>
<ArrowUpRight size={15} />
</span>
<span className="mt-3 block max-w-md pl-[29px] text-sm leading-relaxed text-muted-foreground">
{f.text}
</span>
</button>
))}
</div>
<div
className="flex min-h-[330px] flex-col justify-center rounded-xl bg-muted/60 p-6 md:p-9"
aria-live="polite"
>
{active === 0 ? (
<div>
<div className="mb-6 flex items-center justify-between border-b border-border pb-4">
<span className="text-xs font-medium">Project switcher</span>
<span className="text-xs text-muted-foreground">
2 comments
</span>
</div>
{[
[
"JL",
"Jo",
"Could we keep recent projects above the search results? That covers most switches.",
],
[
"SK",
"Sam",
"Yes. I’ll show the last five, with the current project pinned at the top.",
],
].map(([initials, name, text]) => (
<div key={name} className="mb-6 flex gap-3">
<span className="grid size-7 shrink-0 place-items-center rounded-full bg-background text-[10px]">
{initials}
</span>
<div>
<p className="text-xs font-medium">{name}</p>
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
{text}
</p>
</div>
</div>
))}
<div className="flex items-center gap-2 border-t border-border pt-4 text-xs text-primary">
<Check size={14} /> Decision recorded in the project brief
</div>
</div>
) : active === 1 ? (
<div>
<h3 className="mb-6 text-lg">A handoff without the meeting.</h3>
{[
["Design review", "Jo Lee", "Complete"],
["Implementation", "Sam Kim", "In progress"],
["Release notes", "Alex Morgan", "Up next"],
].map(([step, owner, status], i) => (
<div
key={step}
className="flex items-center gap-4 border-t border-border py-5"
>
<span
className={cn(
"grid size-8 place-items-center rounded-full text-xs",
i === 1
? "bg-primary text-primary-foreground"
: "bg-background",
)}
>
{i === 0 ? (
<Check size={14} />
) : (
owner
.split(" ")
.map((n) => n[0])
.join("")
)}
</span>
<div className="flex-1">
<p className="text-sm font-medium">{step}</p>
<p className="mt-1 text-xs text-muted-foreground">
{owner}
</p>
</div>
<span className="text-[11px] text-muted-foreground">
{status}
</span>
</div>
))}
</div>
) : (
<div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<FileText size={15} /> Project brief · Updated by Alex
</div>
<h3 className="mb-5 mt-6 text-2xl">Workspace launch</h3>
<p className="text-sm leading-relaxed">
Make it easier to move between projects without losing your
place.
</p>
<h4 className="mb-3 mt-6 text-xs font-medium">
Ready to release when
</h4>
<ul className="grid gap-3 text-xs text-muted-foreground">
{[
"Recent projects are one click away",
"Every action works from the keyboard",
"Empty and loading states are reviewed",
].map((t) => (
<li key={t} className="flex items-center gap-2">
<Check size={13} className="text-primary" />
{t}
</li>
))}
</ul>
</div>
)}
</div>
</div>
</section>
);
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
Dependencies
lucide-react@0.577.0 · clsx@2.1.1 · tailwind-merge@3.5.0
Accessibility & behaviour
Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.
Read the accessibility and performance guide →