Skip to content
goodtabs UI
BrowseBrowse components

Feature sticky

A sticky heading beside a scrolling list of numbered features, in plain CSS.

npx shadcn@latest add https://ui.goodtabs.co/r/feature-sticky.json

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.

  1. 01

    Describe it

    Write the screen in plain words and get a first draft.

  2. 02

    Compose it

    Swap parts for registry components that match your tokens.

  3. 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.

  1. 01

    Side by side

    Before and after share one scroll position.

  2. 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

PropTypeDefaultDescription
eyebrowstringSmall uppercase line above the heading.
titleReactNodeThe sticky heading.
descriptionReactNodeGrey lead under the heading.
actionsReactNodeSlot under the lead, usually a link.
features{ title: string; description: string; visual?: ReactNode }[]The list that scrolls: title, description, optional visual.

Source

feature-sticky/feature-sticky.tsx
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