# Feature Section v3

> Roomy headline with numbered tags on key phrases, plus three theme-aware SVG feature illustrations for funnel health, friction, and experiment impact.

A shadcn/ui block from the [@uiception](https://uiception.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiception/feature-section-v3).

- Page, with a live preview: https://shadcn.exchange/r/uiception/feature-section-v3
- Screenshot: https://shadcn.exchange/shot/uiception/feature-section-v3/light.png
- Registry item: https://uiception.com/r/feature-section-v3.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/uiception/feature-section-v3.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `geist`
- Registry items: `card`

## Files

### `registry/new-york/blocks/feature-section-v3/page.tsx`

```tsx
import FeatureSectionV3 from "./components/feature-section-v3"

export default function Page() {
  return <FeatureSectionV3 />
}
```

### `registry/new-york/blocks/feature-section-v3/components/feature-section-v3.tsx`

```tsx
import FeatureShowcase from "./feature-showcase"
import "../styles/feature-section-v3.css"

export default function FeatureSectionV3() {
  return (
    <section className="py-4 md:py-6 lg:py-8">
      <div className="mx-auto max-w-6xl px-4">
        <h2 className="max-w-3xl text-3xl leading-tight font-medium tracking-tighter sm:text-4xl lg:text-5xl">
          Know what&apos;s happening in your product
        </h2>
        <p className="mt-4 max-w-2xl text-3.75 leading-relaxed text-muted-foreground">
          Finds opportunities in your product, understands what&apos;s driving
          behavior, and delivers growth from insight to impact.
        </p>

        <div className="mt-12 lg:mt-16">
          <FeatureShowcase />
        </div>
      </div>
    </section>
  )
}
```

### `registry/new-york/blocks/feature-section-v3/components/feature-showcase.tsx`

```tsx
import type { ComponentType } from "react"

import { GeistMono } from "geist/font/mono"

import { Card, CardContent, CardTitle } from "@/components/ui/card"

import DropoffIllustration from "./dropoff-illustration"
import ExperimentIllustration from "./experiment-illustration"
import { FeatureCardMedia } from "./feature-card-media"
import FunnelIllustration from "./funnel-illustration"
import { features, type FeatureIllustrationId } from "../lib/features"

const illustrationComponents: Record<FeatureIllustrationId, ComponentType> = {
  "funnel-opportunities": FunnelIllustration,
  "dropoff-diagnosis": DropoffIllustration,
  "experiment-impact": ExperimentIllustration,
}

export default function FeatureShowcase() {
  return (
    <ul className="m-0 grid list-none grid-cols-1 gap-4 p-0 md:grid-cols-3">
      {features.map(({ id, step, title, description, illustration }) => {
        const Illustration = illustrationComponents[illustration]

        return (
          <li key={id} className="flex min-w-0 flex-col">
            <Card className="group/card flex h-full min-h-80 flex-col gap-0 py-0">
              <FeatureCardMedia>
                <Illustration />
              </FeatureCardMedia>
              <CardContent className="flex flex-col gap-0 px-3.5 pt-3 pb-2.25">
                <p
                  className={`${GeistMono.className} m-0 mb-1 shrink-0 text-2.75 leading-[1.2] font-medium tracking-[0.14em] text-foreground/85`}
                >
                  {step}
                </p>
                <CardTitle className="m-0 mb-1.25 min-h-[calc(0.95rem*1.35*2)] text-3.8 leading-[1.35] font-semibold tracking-normal text-foreground">
                  {title}
                </CardTitle>
                <p className="m-0 shrink-0 text-3.25 leading-normal text-muted-foreground">
                  {description}
                </p>
              </CardContent>
            </Card>
          </li>
        )
      })}
    </ul>
  )
}
```

### `registry/new-york/blocks/feature-section-v3/components/feature-illustration.tsx`

```tsx
export const FEATURE_CARD_MEDIA_HEIGHT = "h-56 min-h-56"
```

### `registry/new-york/blocks/feature-section-v3/components/feature-card-media.tsx`

```tsx
import type { ReactNode } from "react"

import { cn } from "@/lib/utils"

import { FEATURE_CARD_MEDIA_HEIGHT } from "./feature-illustration"

type FeatureCardMediaProps = {
  children: ReactNode
  className?: string
}

export function FeatureCardMedia({ children, className }: FeatureCardMediaProps) {
  return (
    <div
      className={cn(
        "flex w-full shrink-0 items-center justify-center border-b border-border/60 bg-muted/20 p-4",
        FEATURE_CARD_MEDIA_HEIGHT,
        className
      )}
      aria-hidden
    >
      <div className="h-full max-h-full w-full max-w-full min-h-0 overflow-hidden">
        {children}
      </div>
    </div>
  )
}
```

### `registry/new-york/blocks/feature-section-v3/components/funnel-illustration.tsx`

```tsx
"use client"

import { cn } from "@/lib/utils"

import { MockScreen } from "./mock-screen"
import { useInViewOnce } from "../lib/use-in-view-once"

const WEDGES: ReadonlyArray<{
  label: string
  users: string
  width: string
  gap?: boolean
}> = [
  { label: "Visit", users: "12.4k", width: "92%" },
  { label: "Signup", users: "4.1k", width: "74%" },
  { label: "Activate", users: "1.8k", width: "52%", gap: true },
  { label: "Paid", users: "640", width: "34%" },
]

export default function FunnelIllustration() {
  const { ref, active } = useInViewOnce()

  return (
    <div ref={ref} className="size-full min-h-0 p-1">
      <MockScreen
        label="Funnel stages with a drop-off gap on Activate"
        className={cn("fsv3-fn-mock", active && "fsv3-fn-mock--active")}
      >
        <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-1.5 px-3 py-3">
          {WEDGES.map((wedge, index) => (
            <div
              key={wedge.label}
              className={cn(
                "fsv3-fn-wedge relative flex w-full flex-col items-center",
                `fsv3-fn-wedge--${index + 1}`,
                wedge.gap &&
                  "origin-center motion-safe:transition-transform duration-200 ease-out group-hover/card:scale-[1.03]"
              )}
            >
              <div
                className={cn(
                  "flex h-7 items-center justify-between gap-2 rounded-sm border border-border/65 bg-muted/35 px-2",
                  wedge.gap && "border-foreground/40 bg-muted/65"
                )}
                style={{ width: wedge.width }}
              >
                <span className="font-mono text-[0.5625rem] text-foreground/80">
                  {wedge.label}
                </span>
                <span className="font-mono text-[0.5625rem] text-muted-foreground tabular-nums">
                  {wedge.users}
                </span>
              </div>
              {wedge.gap ? (
                <span className="fsv3-fn-gap-mark mt-0.5 font-mono text-[0.5rem] tracking-[0.12em] text-foreground/70 uppercase">
                  gap · −56%
                </span>
              ) : null}
            </div>
          ))}
        </div>
      </MockScreen>
    </div>
  )
}
```

### `registry/new-york/blocks/feature-section-v3/components/dropoff-illustration.tsx`

```tsx
"use client"

import { cn } from "@/lib/utils"

import { MockScreen } from "./mock-screen"
import { useInViewOnce } from "../lib/use-in-view-once"

const STOPS: ReadonlyArray<{
  label: string
  stuck?: boolean
}> = [
  { label: "Land" },
  { label: "Price" },
  { label: "Pay", stuck: true },
  { label: "Buy" },
]

export default function DropoffIllustration() {
  const { ref, active } = useInViewOnce()

  return (
    <div ref={ref} className="size-full min-h-0 p-1">
      <MockScreen
        label="People leave at checkout because the form feels hard"
        className={cn("fsv3-do-mock", active && "fsv3-do-mock--active")}
      >
        <div className="flex min-h-0 flex-1 flex-col justify-between px-3 py-3">
          <div className="fsv3-do-path relative flex items-start justify-between gap-1 pt-1">
            <div
              className="absolute top-[0.7rem] right-4 left-4 h-px bg-border"
              aria-hidden
            />
            {STOPS.map((stop, index) => (
              <div
                key={stop.label}
                className={cn(
                  "fsv3-do-stop relative z-10 flex flex-1 flex-col items-center gap-1.5",
                  `fsv3-do-stop--${index + 1}`
                )}
              >
                <span
                  className={cn(
                    "flex size-5 items-center justify-center rounded-full border border-border bg-card",
                    stop.stuck &&
                      "size-6 border-foreground/50 bg-foreground text-background motion-safe:transition-transform duration-200 ease-out group-hover/card:scale-110"
                  )}
                >
                  {stop.stuck ? (
                    <span className="font-mono text-[0.5rem] font-semibold">!</span>
                  ) : (
                    <span className="size-1.5 rounded-full bg-foreground/35" />
                  )}
                </span>
                <span
                  className={cn(
                    "font-mono text-[0.5rem] text-muted-foreground",
                    stop.stuck && "font-medium text-foreground/85"
                  )}
                >
                  {stop.label}
                </span>
              </div>
            ))}
          </div>

          <div
            className={cn(
              "fsv3-do-pin relative mx-auto w-full max-w-[11.5rem] rounded-lg border border-foreground/30 bg-muted/50 px-2.5 py-2",
              "motion-safe:transition-transform duration-200 ease-out group-hover/card:-translate-y-0.5"
            )}
          >
            <span
              className="absolute -top-1.5 left-1/2 size-2.5 -translate-x-1/2 rotate-45 border-t border-l border-foreground/30 bg-muted/50"
              aria-hidden
            />
            <p className="font-mono text-[0.5rem] tracking-[0.1em] text-muted-foreground uppercase">
              Why they leave
            </p>
            <p className="mt-0.5 text-[0.6875rem] leading-snug font-medium tracking-tight text-foreground">
              Checkout form feels too long
            </p>
            <p className="mt-1 font-mono text-[0.5625rem] text-foreground/70">
              7 of 10 exits
            </p>
          </div>
        </div>
      </MockScreen>
    </div>
  )
}
```

### `registry/new-york/blocks/feature-section-v3/components/experiment-illustration.tsx`

```tsx
"use client"

import { cn } from "@/lib/utils"

import { MockScreen } from "./mock-screen"
import { useInViewOnce } from "../lib/use-in-view-once"

function CheckoutDraft({
  fields,
  winner,
}: {
  fields: number
  winner?: boolean
}) {
  return (
    <div
      className={cn(
        "flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border",
        winner
          ? "border-foreground/45 bg-card"
          : "border-border/50 bg-muted/15 opacity-60"
      )}
    >
      <div className="flex h-3 shrink-0 items-center gap-0.5 border-b border-border/45 px-1.5">
        <span className="size-1 rounded-full bg-foreground/25" />
        <span className="size-1 rounded-full bg-foreground/25" />
        <span className="size-1 rounded-full bg-foreground/25" />
      </div>
      <div className="flex min-h-0 flex-1 flex-col justify-end gap-1 p-1.5">
        {Array.from({ length: fields }, (_, i) => (
          <div
            key={i}
            className="h-1.5 w-full rounded-[2px] border border-border/50 bg-muted/40"
          />
        ))}
        <div
          className={cn(
            "mt-0.5 flex h-3.5 items-center justify-center rounded-[2px]",
            winner ? "bg-foreground" : "bg-foreground/20"
          )}
        >
          <span
            className={cn(
              "font-mono text-[0.4375rem] tracking-wide uppercase",
              winner ? "text-background" : "text-foreground/40"
            )}
          >
            Pay
          </span>
        </div>
      </div>
    </div>
  )
}

export default function ExperimentIllustration() {
  const { ref, active } = useInViewOnce()

  return (
    <div ref={ref} className="size-full min-h-0 p-1">
      <MockScreen
        label="New short checkout beats the old long one"
        className={cn("fsv3-ex-mock", active && "fsv3-ex-mock--active")}
      >
        <div className="flex min-h-0 flex-1 flex-col gap-2 px-2.5 py-2.5">
          <div className="grid min-h-0 flex-1 grid-cols-[1fr_auto_1fr] items-stretch gap-1.5">
            <div className="fsv3-ex-side fsv3-ex-side--a flex min-h-0 flex-col gap-1">
              <p className="shrink-0 font-mono text-[0.5rem] tracking-[0.1em] text-muted-foreground uppercase">
                A · Old
              </p>
              <CheckoutDraft fields={4} />
            </div>

            <div
              className="fsv3-ex-vs flex shrink-0 items-center self-center font-mono text-[0.5rem] text-muted-foreground"
              aria-hidden
            >
              vs
            </div>

            <div className="fsv3-ex-side fsv3-ex-side--b flex min-h-0 flex-col gap-1">
              <div className="flex shrink-0 items-center justify-between gap-1">
                <p className="font-mono text-[0.5rem] tracking-[0.1em] text-foreground/80 uppercase">
                  B · New
                </p>
                <span className="rounded-[2px] bg-foreground px-1 py-px font-mono text-[0.4375rem] font-semibold tracking-wide text-background uppercase">
                  Win
                </span>
              </div>
              <CheckoutDraft fields={1} winner />
            </div>
          </div>

          <div
            className={cn(
              "fsv3-ex-result flex shrink-0 items-baseline justify-between gap-2 rounded-md border border-foreground/30 bg-muted/55 px-2.5 py-2",
              "motion-safe:transition-transform duration-200 ease-out group-hover/card:-translate-y-0.5"
            )}
          >
            <div>
              <p className="font-mono text-[0.5rem] tracking-[0.1em] text-muted-foreground uppercase">
                Result
              </p>
              <p className="mt-0.5 text-[0.75rem] leading-none font-medium tracking-tight text-foreground">
                +18% more sales
              </p>
            </div>
            <p className="font-mono text-[0.5rem] tracking-[0.08em] text-foreground/70 uppercase">
              Shipped
            </p>
          </div>
        </div>
      </MockScreen>
    </div>
  )
}
```

### `registry/new-york/blocks/feature-section-v3/components/mock-screen.tsx`

```tsx
import type { ReactNode } from "react"

import { cn } from "@/lib/utils"

type MockScreenProps = {
  children: ReactNode
  className?: string
  label: string
}

export function MockScreen({ children, className, label }: MockScreenProps) {
  return (
    <div
      className={cn(
        "fsv3-mock flex size-full min-h-0 flex-col overflow-hidden rounded-xl border border-border/70 bg-card text-foreground opacity-70 shadow-sm motion-safe:transition-opacity duration-200 ease-out group-hover/card:opacity-100",
        className
      )}
      role="img"
      aria-label={label}
    >
      {children}
    </div>
  )
}
```

### `registry/new-york/blocks/feature-section-v3/lib/use-in-view-once.ts`

```ts
"use client"

import { useEffect, useRef, useState } from "react"

export function useInViewOnce(threshold = 0.35) {
  const ref = useRef<HTMLDivElement>(null)
  const [active, setActive] = useState(false)

  useEffect(() => {
    const el = ref.current
    if (!el || active) return

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry?.isIntersecting) {
          setActive(true)
          observer.disconnect()
        }
      },
      { threshold }
    )

    observer.observe(el)
    return () => observer.disconnect()
  }, [active, threshold])

  return { ref, active }
}
```

### `registry/new-york/blocks/feature-section-v3/lib/features.ts`

```ts
export type FeatureIllustrationId =
  | "funnel-opportunities"
  | "dropoff-diagnosis"
  | "experiment-impact"

export type Feature = {
  id: FeatureIllustrationId
  step: string
  title: string
  description: string
  illustration: FeatureIllustrationId
}

export const features: Feature[] = [
  {
    id: "funnel-opportunities",
    step: "01",
    title: "Find opportunities hiding in your funnel",
    description:
      "Follows users through your funnel so gaps and odd behavior surface before they cost you.",
    illustration: "funnel-opportunities",
  },
  {
    id: "dropoff-diagnosis",
    step: "02",
    title: "Understand what's driving the drop-off",
    description:
      "Points to exactly where users stall in your flow and what's sending them away.",
    illustration: "dropoff-diagnosis",
  },
  {
    id: "experiment-impact",
    step: "03",
    title: "Growth experiments from insight to impact",
    description:
      "Transforms insights into real experiments and follows the results until you see impact.",
    illustration: "experiment-impact",
  },
]
```

### `registry/new-york/blocks/feature-section-v3/styles/feature-section-v3.css`

```css
@keyframes fsv3-mock-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fsv3-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fsv3-pop {
  from {
    opacity: 0;
    transform: scale(0.92);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.fsv3-fn-wedge,
.fsv3-fn-gap-mark,
.fsv3-do-stop,
.fsv3-do-pin,
.fsv3-ex-side,
.fsv3-ex-vs,
.fsv3-ex-result {
  opacity: 0;
}

.fsv3-fn-mock--active .fsv3-fn-wedge--1 {
  animation: fsv3-mock-in 400ms cubic-bezier(0.22, 1, 0.36, 1) 60ms forwards;
}

.fsv3-fn-mock--active .fsv3-fn-wedge--2 {
  animation: fsv3-mock-in 400ms cubic-bezier(0.22, 1, 0.36, 1) 140ms forwards;
}

.fsv3-fn-mock--active .fsv3-fn-wedge--3 {
  animation: fsv3-mock-in 460ms cubic-bezier(0.22, 1, 0.36, 1) 220ms forwards;
}

.fsv3-fn-mock--active .fsv3-fn-wedge--4 {
  animation: fsv3-mock-in 400ms ease-out 300ms forwards;
}

.fsv3-fn-mock--active .fsv3-fn-gap-mark {
  animation: fsv3-fade-in 360ms ease-out 480ms forwards;
}

.fsv3-do-mock--active .fsv3-do-stop--1 {
  animation: fsv3-mock-in 320ms ease-out 60ms forwards;
}

.fsv3-do-mock--active .fsv3-do-stop--2 {
  animation: fsv3-mock-in 320ms ease-out 120ms forwards;
}

.fsv3-do-mock--active .fsv3-do-stop--3 {
  animation: fsv3-pop 420ms cubic-bezier(0.22, 1, 0.36, 1) 200ms forwards;
}

.fsv3-do-mock--active .fsv3-do-stop--4 {
  animation: fsv3-mock-in 320ms ease-out 280ms forwards;
}

.fsv3-do-mock--active .fsv3-do-pin {
  animation: fsv3-mock-in 420ms cubic-bezier(0.22, 1, 0.36, 1) 380ms forwards;
}

.fsv3-ex-mock--active .fsv3-ex-side--a {
  animation: fsv3-mock-in 360ms ease-out 80ms forwards;
}

.fsv3-ex-mock--active .fsv3-ex-vs {
  animation: fsv3-fade-in 280ms ease-out 160ms forwards;
}

.fsv3-ex-mock--active .fsv3-ex-side--b {
  animation: fsv3-pop 420ms cubic-bezier(0.22, 1, 0.36, 1) 220ms forwards;
}

.fsv3-ex-mock--active .fsv3-ex-result {
  animation: fsv3-mock-in 400ms cubic-bezier(0.22, 1, 0.36, 1) 380ms forwards;
}

@media (prefers-reduced-motion: reduce) {
  .fsv3-fn-wedge,
  .fsv3-fn-gap-mark,
  .fsv3-do-stop,
  .fsv3-do-pin,
  .fsv3-ex-side,
  .fsv3-ex-vs,
  .fsv3-ex-result {
    opacity: 1;
    transform: none;
    animation: none;
  }
}
```
