Feature sticky
A sticky heading beside a scrolling list of numbered features, in plain CSS.
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/feature-sticky
Workflow
From idea to merged in one sitting
Each step keeps the one before it visible.
- 01
Describe it
Write the screen in plain words and get a first draft.
- 02
Compose it
Swap parts for registry components that match your tokens.
- 03
Review it
Check contrast, focus and motion with one command.
Sticky heading with three numbered steps, in a scrolling box.
With visuals
A review that reads like a diff
See what changed, why, and who needs to look.
- 01
Side by side
Before and after share one scroll position.
- 02
Inline comments
Threads stay anchored to the line they started on.
Each feature carries a visual slot under its text.
Usage
import { FeatureSticky } from "@/components/feature-sticky"
<FeatureSticky
title="From idea to merged in one sitting"
features={[
{ title: "Describe it", description: "Plain words in, first draft out." },
{ title: "Review it", description: "Check contrast and focus." },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| eyebrow | string | Small uppercase line above the heading. | |
| title | ReactNode | The sticky heading. | |
| description | ReactNode | Grey lead under the heading. | |
| actions | ReactNode | Slot under the lead, usually a link. | |
| features | { title: string; description: string; visual?: ReactNode }[] | The list that scrolls: title, description, optional visual. |
Source
import * as React from "react";
import { cn } from "@/lib/utils";
export type StickyFeature = { title: string; description: string; visual?: React.ReactNode };
type FeatureStickyProps = Omit<React.ComponentProps<"section">, "title"> & {
/** Eyebrow above the sticky heading. */
eyebrow?: string;
/** The sticky heading. */
title: React.ReactNode;
/** Grey lead under the heading. */
description?: React.ReactNode;
/** Slot under the lead, usually a link. */
actions?: React.ReactNode;
/** The list that scrolls beside the heading. */
features: StickyFeature[];
};
/**
* A heading that stays put while a list of features scrolls beside it.
* Plain CSS sticky, no script. Side by side from the container's 2xl width, stacked below.
* Sticky needs no overflow clipping on any ancestor. Server component.
*/
export function FeatureSticky({
eyebrow,
title,
description,
actions,
features,
className,
...props
}: FeatureStickyProps) {
return (
<section className={cn("@container w-full py-8 @2xl:py-16", className)} {...props}>
<div className="mx-auto grid w-full max-w-6xl gap-8 px-4 @2xl:grid-cols-[5fr_7fr] @2xl:gap-16 @2xl:px-6">
<div className="@2xl:sticky @2xl:top-24 @2xl:self-start">
<div className="flex flex-col items-start gap-3">
{eyebrow && (
<p className="m-0 font-mono text-xs tracking-wide text-muted-foreground uppercase">{eyebrow}</p>
)}
<h2 className="m-0 text-2xl leading-tight font-medium tracking-tight text-balance @2xl:text-4xl">
{title}
</h2>
{description && <p className="m-0 text-sm text-muted-foreground @2xl:text-base">{description}</p>}
{actions}
</div>
</div>
<ol className="m-0 flex list-none flex-col p-0">
{features.map((f, i) => (
<li key={f.title} className="flex flex-col gap-3 border-t py-6 first:border-t-0 first:pt-0 @2xl:py-10">
<span className="font-mono text-xs text-muted-foreground">{String(i + 1).padStart(2, "0")}</span>
<h3 className="m-0 text-base font-medium tracking-tight @2xl:text-xl">{f.title}</h3>
<p className="m-0 max-w-md text-sm text-muted-foreground @2xl:text-base">{f.description}</p>
{f.visual && <div className="mt-2">{f.visual}</div>}
</li>
))}
</ol>
</div>
</section>
);
}
For AI agents
Hand your agent this page, the markdown at /components/feature-sticky/prompt.md, or the Copy AI prompt button above.
When to use
- Three to six features that each need a paragraph or a visual
- When the heading should stay in view while the reader works down
- Long pages where a grid would be too shallow
When not to use
- Short one-line points, which want the feature grid
- Scroll-driven stories that swap content, which want a pinned story
- One or two features, where there is nothing to scroll past
Agent notes
- Pure CSS position: sticky from the container's @2xl width; stacked and not sticky below
- Sticky breaks if any ancestor has overflow hidden or auto; the section sets none
- Stuck at top-24 to clear a 64px site header; change it in the source if your header differs
- Server component; the numbers 01, 02 come from the array index