# Blocks Showcase

> Blocks Showcase variant for block.

A shadcn/ui block from the [@uiable](https://uiable.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiable/block-blocks-showcase).

- Page, with a live preview: https://shadcn.exchange/r/uiable/block-blocks-showcase
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uiable/block-blocks-showcase-light-860282add7b9ac6e.png
- Registry item: https://uiable.com/r/block-blocks-showcase.json
- Categories: landing
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uiable.com/r/block-blocks-showcase.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: `@tabler/icons-react`, `cn`
- Registry items: `@uiable/badge`

## Files

### `src/components/uiable/blocks/landing/blocks-showcase/blocks-showcase.tsx`

```tsx
// next
import Link from "next/link"

// shadcn
import { Badge } from "@/components/ui/badge"

// third-party
import { cn } from "cn"

// project-imports
import categoryCounts from "@/category-counts.json"
import AutoScrollPreview from "@/components/animation/AutoScrollPreview"
import CrossfadeImages from "@/components/animation/CrossfadeImages"
import MarqueeImages from "@/components/animation/MarqueeImages"
import TestimonialsMarquee from "@/components/animation/TestimonialsMarquee"
import SectionHeader from "@/components/uiable/blocks/landing/components/SectionHeader"

// assets
import {
  IconArrowUpRight,
  IconChartBar,
  IconLayoutBoardSplit,
  IconLayoutGrid,
  IconShoppingCart,
  IconUsersGroup,
  type Icon as TablerIcon,
} from "@tabler/icons-react"

// ------------------------------ | CONSTANTS | ------------------------------ //

const bentoPreviews = [
  {
    src: "https://cdn.uiable.com/block/img-bento-1.png",
    darkSrc: "https://cdn.uiable.com/block/img-bento-1-dark.png",
  },
  {
    src: "https://cdn.uiable.com/block/img-bento-2.png",
  },
  {
    src: "https://cdn.uiable.com/block/img-bento-3.png",
    darkSrc: "https://cdn.uiable.com/block/img-bento-3-dark.png",
  },
  {
    src: "https://cdn.uiable.com/block/img-bento-4.png",
    darkSrc: "https://cdn.uiable.com/block/img-bento-4-dark.png",
  },
  {
    src: "https://cdn.uiable.com/block/img-bento-5.png",
    darkSrc: "https://cdn.uiable.com/block/img-bento-5-dark.png",
  },
  {
    src: "https://cdn.uiable.com/block/img-bento-6.png",
    darkSrc: "https://cdn.uiable.com/block/img-bento-6-dark.png",
  },
]

const heroPreviews = [
  "https://cdn.uiable.com/block/hero-1.gif",
  "https://cdn.uiable.com/block/hero-2.gif",
  "https://cdn.uiable.com/block/hero-3.png",
  "https://cdn.uiable.com/block/hero-5.png",
  "https://cdn.uiable.com/block/hero-4.png",
]

const ecommercePreviews = [
  {
    src: "https://cdn.uiable.com/block/e-commerce-1-light.png",
    darkSrc: "https://cdn.uiable.com/block/e-commerce-1-dark.png",
  },
  {
    src: "https://cdn.uiable.com/block/e-commerce-2-light.png",
    darkSrc: "https://cdn.uiable.com/block/e-commerce-2-dark.png",
  },
  {
    src: "https://cdn.uiable.com/block/e-commerce-3-light.png",
    darkSrc: "https://cdn.uiable.com/block/e-commerce-3-dark.png",
  },
  {
    src: "https://cdn.uiable.com/block/e-commerce-4-light.png",
    darkSrc: "https://cdn.uiable.com/block/e-commerce-4-dark.png",
  },
  {
    src: "https://cdn.uiable.com/block/e-commerce-5-light.png",
    darkSrc: "https://cdn.uiable.com/block/e-commerce-5-dark.png",
  },
]

const teamPreview = {
  src: "https://cdn.uiable.com/block/team-light-1.png",
  darkSrc: "https://cdn.uiable.com/block/team-dark-1.png",
}

const testimonialPreview = {
  src: "https://cdn.uiable.com/block/testimonial-light-1.png",
  darkSrc: "https://cdn.uiable.com/block/testimonial-dark-1.png",
}

type Preview =
  | { kind: "static"; src: string; darkSrc?: string }
  | { kind: "scroll"; src: string }
  | {
      kind: "crossfade"
      images: (string | { src: string; darkSrc?: string })[]
    }
  | { kind: "marquee"; images: (string | { src: string; darkSrc?: string })[] }
  | {
      kind: "vscroll"
      images: (string | { src: string; darkSrc?: string })[]
    }

interface ShowcaseBlock {
  slug: string
  title: string
  description: string
  icon: TablerIcon
  className: string
  preview: Preview
}

const blocks: ShowcaseBlock[] = [
  // ---- Left column: two equal-size cards ----
  {
    slug: "bento",
    title: "Bento grids",
    description: "Asymmetric feature mosaics for product pages.",
    icon: IconLayoutBoardSplit,
    className: "lg:col-start-1 lg:row-start-1 lg:row-span-3",
    // Animated: screenshots cross-fade on a gentle loop.
    preview: { kind: "crossfade", images: bentoPreviews },
  },
  {
    slug: "hero",
    title: "Hero sections",
    description:
      "Opening bands with headline, supporting copy, and primary actions.",
    icon: IconLayoutGrid,
    className: "lg:col-start-1 lg:row-start-4 lg:row-span-6",
    // Animated: a strip of screenshots scrolls up forever (light/dark pair per screenshot).
    preview: { kind: "vscroll", images: heroPreviews },
  },
  // ---- Right column: three equal-size cards ----
  {
    slug: "team",
    title: "Team blocks",
    description: "Team grids and member cards with roles, bios, and socials.",
    icon: IconUsersGroup,
    className: "lg:col-start-2 lg:row-start-1 lg:row-span-3",
    preview: { kind: "static", ...teamPreview },
  },
  {
    slug: "e-commerce",
    title: "E-commerce",
    description: "Product grids, cards, and cart layouts.",
    icon: IconShoppingCart,
    className: "lg:col-start-2 lg:row-start-4 lg:row-span-3",
    // Animated: a strip of screenshots scrolls left forever.
    preview: { kind: "marquee", images: ecommercePreviews },
  },
  {
    slug: "statistics",
    title: "Statistics",
    description:
      "Key metrics, stat cards, and numerical displays to showcase impact.",
    icon: IconChartBar,
    className: "lg:col-start-2 lg:row-start-7 lg:row-span-3",
    preview: { kind: "static", ...testimonialPreview },
  },
]

const counts = categoryCounts as Record<string, number>

// ------------------------------ | PREVIEW | ------------------------------ //

function BlockPreview({ preview, title }: { preview: Preview; title: string }) {
  const alt = `${title} preview`

  switch (preview.kind) {
    case "crossfade":
      return <CrossfadeImages images={preview.images} alt={alt} />
    case "scroll":
      return <AutoScrollPreview src={preview.src} alt={alt} className="m-2" />
    case "marquee":
      return (
        <MarqueeImages images={preview.images} alt={alt} className="pb-2" />
      )
    case "vscroll":
      return <TestimonialsMarquee images={preview.images} alt={alt} />
    case "static":
      return (
        <div className="flex h-full min-h-[150px] w-full items-start justify-center bg-white px-3 pb-3 dark:bg-card">
          <img
            src={preview.src}
            alt={alt}
            loading="lazy"
            className={cn(
              "h-auto max-h-full w-auto max-w-full rounded-lg object-contain",
              preview.darkSrc && "dark:hidden"
            )}
          />
          {preview.darkSrc && (
            <img
              src={preview.darkSrc}
              alt={alt}
              loading="lazy"
              className="hidden h-auto max-h-full w-auto max-w-full rounded-lg object-contain dark:block"
            />
          )}
        </div>
      )
  }
}

// ------------------------------ | BLOCKS SHOWCASE | ------------------------------ //

function BlockCard({ block }: { block: ShowcaseBlock }) {
  const Icon = block.icon
  const count = counts[block.slug] ?? 0

  return (
    <Link
      href={`/blocks/${block.slug}`}
      aria-label={`Browse ${block.title} blocks`}
      className={cn(
        "group relative flex min-h-[180px] flex-col gap-2 overflow-hidden rounded-xl border bg-card transition-transform focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
        block.className
      )}
    >
      <div className="flex items-center gap-2 px-3 pt-2.5">
        <div className="flex size-7 shrink-0 items-center justify-center rounded-md bg-secondary text-foreground">
          <Icon className="size-3.5" aria-hidden="true" />
        </div>
        <div className="flex flex-1 items-center justify-between gap-2">
          <h3 className="text-sm leading-none font-medium text-foreground">
            {block.title}
          </h3>
          <span className="relative flex min-w-5 shrink-0 items-center justify-center">
            {count > 0 && (
              <Badge
                variant="outline"
                className="rounded-full px-1.5 py-0 text-[11px] transition-opacity group-hover:opacity-0"
              >
                {count}
              </Badge>
            )}
            <IconArrowUpRight
              className="absolute size-4 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
              aria-hidden="true"
            />
          </span>
        </div>
      </div>

      <div className="mt-auto flex w-full min-w-0 flex-1 flex-col lg:min-h-0">
        <BlockPreview preview={block.preview} title={block.title} />
      </div>
    </Link>
  )
}

export default function BlocksShowcase() {
  return (
    <section className="mx-auto flex w-full flex-col gap-12.5 px-4 py-12.5 sm:px-8">
      <SectionHeader
        title="More than basic Shadcn blocks"
        titleClassName="tracking-tight"
        subtitle="Explore beautifully crafted Shadcn blocks for landing pages, dashboards, and more. Just copy, paste, and make them yours."
      />

      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-2 lg:grid-rows-[repeat(9,7rem)]">
        {blocks.map((block) => (
          <BlockCard key={block.slug} block={block} />
        ))}
      </div>
    </section>
  )
}
```

### `src/category-counts.json`

```json
{
  "accordion": 10,
  "alert": 46,
  "alert-dialog": 10,
  "aspect-ratio": 5,
  "attachment": 5,
  "avatar": 15,
  "badge": 37,
  "layout": 14,
  "breadcrumb": 11,
  "bubble": 8,
  "button": 49,
  "button-group": 15,
  "calendar": 12,
  "card": 24,
  "carousel": 9,
  "changelog": 3,
  "chart": 10,
  "checkbox": 11,
  "collapsible": 6,
  "combobox": 14,
  "command": 9,
  "context-menu": 13,
  "coupon": 3,
  "cursor": 3,
  "data-table": 4,
  "date-picker": 8,
  "dialog": 15,
  "drawer": 6,
  "dropdown-menu": 25,
  "empty": 8,
  "field": 12,
  "hover-card": 5,
  "input": 16,
  "input-group": 22,
  "input-otp": 11,
  "item": 15,
  "kbd": 7,
  "label": 4,
  "list-group": 13,
  "marker": 7,
  "menubar": 8,
  "message": 5,
  "message-scroller": 9,
  "native-select": 3,
  "navigation-menu": 3,
  "pagination": 11,
  "popover": 8,
  "progress": 20,
  "questionnaire": 7,
  "radio": 1,
  "radio-group": 9,
  "resizable": 7,
  "scroll-area": 5,
  "select": 10,
  "separator": 8,
  "sheet": 5,
  "sidebar": 17,
  "skeleton": 8,
  "slider": 8,
  "sonner": 8,
  "spinner": 21,
  "switch": 11,
  "table": 6,
  "tabs": 10,
  "textarea": 8,
  "timeline": 7,
  "toggle": 6,
  "toggle-group": 8,
  "tooltip": 11,
  "tree-view": 7,
  "typography": 22,
  "contact": 24,
  "content": 17,
  "cta": 12,
  "e-commerce": 25,
  "faq": 19,
  "feature": 42,
  "footer": 21,
  "gallery": 11,
  "hero": 32,
  "landing": 14,
  "portfolio": 11,
  "pricing": 19,
  "process": 12,
  "small-hero": 7,
  "statistics": 13,
  "team": 14,
  "testimonial": 24,
  "bento": 8,
  "charts": 8,
  "check-mail": 2,
  "code-verification": 7,
  "coming-soon": 2,
  "component-layout": 1,
  "dashboard-layout": 3,
  "doc-layout": 1,
  "error-404": 2,
  "error-500": 2,
  "forgot-password": 5,
  "join-waitlist": 1,
  "login": 7,
  "navbar": 11,
  "reset-password": 1,
  "sign-up": 7,
  "tables": 1,
  "under-construction": 2,
  "widgets": 20
}
```

### `src/components/animation/AutoScrollPreview.tsx`

```tsx
"use client"

import { useEffect, useRef } from "react"

// project-imports
import { cn } from "@/lib/utils"

// types

interface AutoScrollPreviewProps {
  /** Image URL — a tall screenshot that scrolls vertically inside the frame. */
  src: string
  alt?: string
  /** Seconds for one full top-to-bottom-and-back scroll cycle. */
  duration?: number
  className?: string
}

//  ------------------------------ | AUTO SCROLL PREVIEW | ------------------------------  //
//  A framed screenshot that slowly pans from top to bottom on a loop, as if a
//  visitor were scrolling the page. Runs continuously.

export default function AutoScrollPreview({
  src,
  alt = "",
  duration = 14,
  className,
}: AutoScrollPreviewProps) {
  const imgRef = useRef<HTMLImageElement>(null)
  const animationRef = useRef<Animation | null>(null)

  useEffect(() => {
    const img = imgRef.current
    if (!img) return

    const start = () => {
      animationRef.current = img.animate(
        [
          { transform: "translateY(0%)" },
          { transform: "translateY(calc(-100% + 120px))" },
          { transform: "translateY(0%)" },
        ],
        {
          duration: duration * 1000,
          iterations: Infinity,
          easing: "ease-in-out",
        }
      )
    }

    if (img.complete) start()
    else img.addEventListener("load", start, { once: true })

    return () => animationRef.current?.cancel()
  }, [src, duration])

  return (
    <div
      className={cn(
        "relative h-full min-h-[110px] w-full overflow-hidden rounded-lg bg-muted/40",
        className
      )}
    >
      <img
        ref={imgRef}
        src={src}
        alt={alt}
        loading="lazy"
        className="absolute inset-x-0 top-0 w-full will-change-transform"
      />
      {/* Soft fade at top and bottom so the pan reads as a window into a page. */}
      <div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-card/40 via-transparent to-card/40" />
    </div>
  )
}
```

### `src/components/animation/CrossfadeImages.tsx`

```tsx
"use client"

import { useEffect, useState } from "react"

// project-imports
import { cn } from "@/lib/utils"

// types

interface CrossfadeImage {
  src: string
  darkSrc?: string
}

interface CrossfadeImagesProps {
  /** Three to five screenshot URLs, shown one at a time on a gentle loop. */
  images: (string | CrossfadeImage)[]
  alt?: string
  /** Milliseconds each screenshot stays on screen before the next fades in. */
  interval?: number
  className?: string
}

//  ------------------------------ | CROSSFADE IMAGES | ------------------------------  //
//  A calm slideshow: framed screenshots cross-fade into one another while drifting
//  in with a slow zoom. No rotation or bounce — it reads as a quiet product tour.

export default function CrossfadeImages({
  images,
  alt = "",
  interval = 3200,
  className,
}: CrossfadeImagesProps) {
  const [active, setActive] = useState(0)

  useEffect(() => {
    if (images.length <= 1) return
    const id = window.setInterval(() => {
      setActive((current) => (current + 1) % images.length)
    }, interval)
    return () => window.clearInterval(id)
  }, [images.length, interval])

  return (
    <div
      className={cn(
        "relative flex h-full w-full items-start bg-white px-3 pb-3 lg:min-h-[150px] lg:items-stretch dark:bg-card",
        className
      )}
    >
      <div className="relative aspect-video w-full overflow-hidden rounded-xl bg-muted/40 lg:aspect-auto">
        {images.map((image, index) => {
          const { src, darkSrc } =
            typeof image === "string"
              ? { src: image, darkSrc: undefined }
              : image
          const isActive = index === active

          return (
            <div key={src} aria-hidden={!isActive}>
              <img
                src={src}
                alt={isActive ? alt : ""}
                loading="lazy"
                className={cn(
                  "absolute inset-0 size-full object-contain object-center transition-[opacity,transform] duration-[1200ms] ease-out will-change-transform motion-reduce:transition-none",
                  isActive ? "scale-100 opacity-100" : "scale-105 opacity-0",
                  darkSrc && "dark:hidden"
                )}
              />
              {darkSrc && (
                <img
                  src={darkSrc}
                  alt={isActive ? alt : ""}
                  loading="lazy"
                  className={cn(
                    "absolute inset-0 hidden size-full object-contain object-center transition-[opacity,transform] duration-[1200ms] ease-out will-change-transform motion-reduce:transition-none dark:block",
                    isActive ? "scale-100 opacity-100" : "scale-105 opacity-0"
                  )}
                />
              )}
            </div>
          )
        })}
      </div>
    </div>
  )
}
```

### `src/components/animation/MarqueeImages.tsx`

```tsx
"use client"

// project-imports
import { cn } from "@/lib/utils"

// types
interface MarqueeImage {
  src: string
  darkSrc?: string
}

interface MarqueeImagesProps {
  images: (string | MarqueeImage)[]
  alt?: string
  duration?: number
  className?: string
}

//  ------------------------------ | MARQUEE IMAGES | ------------------------------  //

export default function MarqueeImages({
  images,
  alt = "",
  duration = 22,
  className,
}: MarqueeImagesProps) {
  const strip = [...images, ...images]

  // Tiles are sized by height + `aspect-video`, so a 16:9 screenshot fills the
  // tile exactly (no crop) and both rows always fit inside the card.
  // The spacing is `mr-3` rather than a flex `gap` so the strip measures exactly
  // 2 x (tile + gap) per pair — `translateX(-50%)` then lands on a seamless
  // boundary instead of half a gap short.
  const tileClass =
    "mr-3 h-24 aspect-video shrink-0 overflow-hidden rounded-md border border-border bg-muted/40 sm:h-28 lg:h-32"

  const row = (direction: "left" | "right") => (
    <div
      className="flex w-max items-center will-change-transform motion-reduce:animate-none"
      style={{ animation: `marquee-${direction} ${duration}s linear infinite` }}
    >
      {strip.map((image, index) => {
        const { src, darkSrc } =
          typeof image === "string" ? { src: image, darkSrc: undefined } : image
        const hidden = index >= images.length

        return (
          <div key={`${direction}-${src}-${index}`} className={tileClass}>
            <img
              src={src}
              alt={hidden ? "" : alt}
              aria-hidden={hidden}
              loading="lazy"
              className={cn(
                "size-full object-contain",
                darkSrc && "dark:hidden"
              )}
            />
            {darkSrc && (
              <img
                src={darkSrc}
                alt={hidden ? "" : alt}
                aria-hidden={hidden}
                loading="lazy"
                className="hidden size-full object-contain dark:block"
              />
            )}
          </div>
        )
      })}
    </div>
  )

  return (
    <div
      className={cn(
        "relative flex h-full min-h-[14rem] w-full items-center overflow-hidden bg-white lg:min-h-[18rem] dark:bg-card",
        className
      )}
    >
      <div className="flex flex-col gap-3">
        {row("left")}
        {row("right")}
      </div>

      <style>{`
        @keyframes marquee-left {
          from { transform: translateX(0); }
          to { transform: translateX(-50%); }
        }
        @keyframes marquee-right {
          from { transform: translateX(-50%); }
          to { transform: translateX(0); }
        }
      `}</style>
    </div>
  )
}
```

### `src/components/animation/TestimonialsMarquee.tsx`

```tsx
"use client"

// project-imports
import { cn } from "@/lib/utils"

// types

interface TestimonialImage {
  src: string

  darkSrc?: string
}

interface TestimonialsMarqueeProps {
  /** One or two screenshots (light/dark pair) — the column is duplicated for a seamless loop. */
  images: (string | TestimonialImage)[]
  alt?: string
  /** Seconds for one full loop. Lower = faster. */
  duration?: number
  className?: string
}

//  ------------------------------ | TESTIMONIALS MARQUEE | ------------------------------  //
export default function TestimonialsMarquee({
  images,
  alt = "",
  duration = 20,
  className,
}: TestimonialsMarqueeProps) {
  const copy = (keyPrefix: string, hidden: boolean) =>
    images.map((image, index) => {
      const { src, darkSrc } =
        typeof image === "string" ? { src: image, darkSrc: undefined } : image

      return (
        <div
          key={`${keyPrefix}-${index}`}
          className="mb-2 h-auto w-full shrink-0 overflow-hidden rounded-lg bg-muted/40"
        >
          <img
            src={src}
            alt={hidden ? "" : alt}
            aria-hidden={hidden}
            loading="lazy"
            className={cn(
              "block h-auto w-full object-contain",
              darkSrc && "dark:hidden"
            )}
          />
          {darkSrc && (
            <img
              src={darkSrc}
              alt={hidden ? "" : alt}
              aria-hidden={hidden}
              loading="lazy"
              className="hidden h-auto w-full object-contain dark:block"
            />
          )}
        </div>
      )
    })

  return (
    <div
      className={cn(
        "group relative h-full min-h-[15rem] w-full overflow-hidden bg-white px-2 pb-2 lg:min-h-[150px] dark:bg-card",
        className
      )}
    >
      <div
        className="absolute inset-x-2 top-0 flex flex-col will-change-transform group-hover:[animation-play-state:paused] motion-reduce:animate-none"
        style={{ animation: `testimonials-up ${duration}s linear infinite` }}
      >
        {copy("a", false)}
        {copy("b", true)}
      </div>

      <style>{`
        @keyframes testimonials-up {
          from { transform: translateY(0); }
          to { transform: translateY(-50%); }
        }
      `}</style>
    </div>
  )
}
```

### `src/components/uiable/blocks/landing/components/SectionHeader.tsx`

```tsx
// third-party
import { cn } from "cn"

// types
interface SectionHeaderProps {
  title: string
  subtitle: string
  className?: string
  titleClassName?: string
  subtitleClassName?: string
}

//  ------------------------------ | BLOCK - SECTION HEADER | ------------------------------  //

export default function SectionHeader({
  title,
  subtitle,
  className,
  titleClassName,
  subtitleClassName,
}: SectionHeaderProps) {
  return (
    <div className={cn("flex flex-col gap-2 md:gap-3", className)}>
      <h2
        className={cn(
          "text-2xl leading-tight font-medium tracking-normal text-foreground sm:text-3xl sm:leading-8 md:text-4xl md:leading-9",
          titleClassName
        )}
      >
        {title}
      </h2>

      <p
        className={cn(
          "text-base leading-relaxed font-normal tracking-normal text-muted-foreground sm:text-lg",
          subtitleClassName
        )}
      >
        {subtitle}
      </p>
    </div>
  )
}
```
