# Feature 03

> A sticky title/description column paired with a scrolling list of features, each with its own title, description, and media.

A shadcn/ui block from the [@flx](https://ui.flexnative.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/flx/feature-03).

- Page, with a live preview: https://shadcn.exchange/r/flx/feature-03
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/flx/feature-03-light-4ff6af61acc7988e.png
- Registry item: https://ui.flexnative.com/r/feature-03.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.flexnative.com/r/feature-03.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: `motion`, `react-wrap-balancer`

## Files

### `registry/blocks/feature/feature-03/feature-03.tsx`

```tsx
'use client'

import * as React from 'react'
import { motion, useReducedMotion, type Variants } from 'motion/react'
import Balancer from 'react-wrap-balancer'

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

export interface Feature03Item {
  id: string
  title: string
  description: string
  media: {
    src: string
    alt: string
  }
}

export interface Feature03Props {
  title: string
  description?: string
  items: Feature03Item[]
  variant?: 'standard' | 'compact'
  animation?: 'none' | 'subtle'
}

const variantStyles = {
  standard: {
    section: 'py-16 sm:py-24',
    grid: 'gap-10 md:gap-16',
    title: 'text-2xl sm:text-3xl md:text-4xl',
    description: 'text-sm sm:text-base',
    itemGap: 'gap-16 sm:gap-24',
    itemTitle: 'text-xl sm:text-2xl',
    itemDescription: 'text-sm sm:text-base',
    media: 'aspect-16/10',
  },
  compact: {
    section: 'py-12 sm:py-16',
    grid: 'gap-8 md:gap-12',
    title: 'text-xl sm:text-2xl md:text-3xl',
    description: 'text-sm',
    itemGap: 'gap-12 sm:gap-16',
    itemTitle: 'text-lg sm:text-xl',
    itemDescription: 'text-sm',
    media: 'aspect-16/11',
  },
} as const

const container: Variants = {
  hidden: {},
  visible: { transition: { staggerChildren: 0.1, delayChildren: 0.05 } },
}

const headerItem: Variants = {
  hidden: { opacity: 0, y: 12, filter: 'blur(6px)' },
  visible: {
    opacity: 1,
    y: 0,
    filter: 'blur(0px)',
    transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
  },
}

const featureItem: Variants = {
  hidden: { opacity: 0, y: 16, filter: 'blur(8px)' },
  visible: {
    opacity: 1,
    y: 0,
    filter: 'blur(0px)',
    transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
  },
}

function Reveal({
  active,
  variants,
  className,
  children,
}: Readonly<{
  active: boolean
  variants?: Variants
  className?: string
  children: React.ReactNode
}>) {
  if (!active) return <div className={className}>{children}</div>

  return (
    <motion.div variants={variants ?? headerItem} className={className}>
      {children}
    </motion.div>
  )
}

function FeatureRow({
  item,
  animate,
  vs,
}: Readonly<{
  item: Feature03Item
  animate: boolean
  vs: (typeof variantStyles)[keyof typeof variantStyles]
}>) {
  const content = (
    <>
      <h3
        className={cn(
          'text-foreground font-semibold tracking-tight',
          vs.itemTitle,
        )}
      >
        <Balancer>{item.title}</Balancer>
      </h3>
      <p className={cn('text-muted-foreground max-w-2xl', vs.itemDescription)}>
        <Balancer>{item.description}</Balancer>
      </p>
      <div
        className={cn(
          'relative w-full overflow-hidden rounded-2xl shadow-sm outline outline-black/10 dark:outline-white/10',
          vs.media,
        )}
      >
        <img
          src={item.media.src}
          alt={item.media.alt || item.title}
          loading="lazy"
          decoding="async"
          className="absolute inset-0 size-full object-cover"
        />
      </div>
    </>
  )

  if (!animate) return <div className="flex flex-col gap-4">{content}</div>

  return (
    <motion.div
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true, margin: '-40px' }}
      variants={featureItem}
      className="flex flex-col gap-4"
    >
      {content}
    </motion.div>
  )
}

export function Feature03({
  title,
  description,
  items,
  variant = 'standard',
  animation = 'none',
}: Readonly<Feature03Props>) {
  const reduce = useReducedMotion()
  const animate = animation === 'subtle' && !reduce
  const vs = variantStyles[variant]

  if (!items.length) return null

  const titleElement = title && (
    <h2
      className={cn('text-foreground font-semibold tracking-tight', vs.title)}
    >
      <Balancer>{title}</Balancer>
    </h2>
  )

  const descriptionElement = description && (
    <p className={cn('text-muted-foreground', vs.description)}>
      <Balancer>{description}</Balancer>
    </p>
  )

  const headerElement = (
    <div className="flex flex-col gap-4 md:sticky md:top-24 md:self-start">
      {titleElement}
      {descriptionElement}
    </div>
  )

  const itemsListElement = (
    <div className={cn('flex flex-col', vs.itemGap)}>
      {items.map((item) => (
        <FeatureRow key={item.id} item={item} animate={animate} vs={vs} />
      ))}
    </div>
  )

  return (
    <section className="bg-background w-full">
      <motion.div
        className={cn('mx-auto w-full max-w-6xl px-6', vs.section)}
        variants={animate ? container : undefined}
        initial={animate ? 'hidden' : false}
        whileInView={animate ? 'visible' : undefined}
        viewport={{ once: true, margin: '-80px' }}
      >
        <div
          className={cn(
            'grid grid-cols-1 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]',
            vs.grid,
          )}
        >
          <Reveal active={animate}>{headerElement}</Reveal>
          {itemsListElement}
        </div>
      </motion.div>
    </section>
  )
}
```

### `registry/blocks/feature/feature-03/feature-03-example.tsx`

```tsx
import { Feature03, type Feature03Props } from './feature-03'

export const values = {
  title: 'Three surfaces, one connected workflow.',
  description:
    'Flexnative brings block browsing, live editing, and code export into a single environment, built around how teams actually ship UI.',
  variant: 'standard',
  animation: 'subtle',
  items: [
    {
      id: '1',
      title: 'Block Library',
      description:
        'Browse hundreds of production-ready blocks across every category, filtered by preset, type, and layout.',
      media: {
        src: 'https://images.unsplash.com/photo-1750044656775-40fd937a2438?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'Soft gradient abstract in warm tones',
      },
    },
    {
      id: '2',
      title: 'Live Editor',
      description:
        'Tweak copy, media, and variants in real time, with an instant preview across breakpoints and presets.',
      media: {
        src: 'https://images.unsplash.com/photo-1441039995991-e5c1178e605a?q=80&w=2053&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'Soft abstract shapes in muted color',
      },
    },
    {
      id: '3',
      title: 'Code Export',
      description:
        "Copy production-ready code or install it via the CLI, typed and documented so it's ready to ship today.",
      media: {
        src: 'https://images.unsplash.com/photo-1640535092659-7856bff6679d?q=80&w=2071&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
        alt: 'Soft warm gradient',
      },
    },
  ],
} satisfies Feature03Props

export function Feature03Example() {
  return <Feature03 {...values} />
}
```
