DocsNewsletter Bookshelf

Newsletter Bookshelf

A tactile WebGL archive with cloth-bound book textures, horizontal camera navigation, hover lift, centered cover inspection, and automatic drag-adjustable orbiting.

Installation

pnpm dlx shadcn@latest add @componentry/newsletter-bookshelf

Dependencies: three @react-three/fiber clsx tailwind-merge

Usage

API Reference

PropTypeDefault
itemsNewsletterBookshelfItem[]

Archive entries with title, date, optional href, and optional cover colors. Activate a focused book again to open its href.

—
heightnumber | string

Height of the WebGL stage.

620
brandstring

Wordmark printed on every generated cover.

"The Brief"
onSelect(item, index) => void

Called when a book enters the focused view.

—
classNamestring

Additional classes for the outer shelf stage.

—

Keep in mind

This component is inspired by various open-source projects and patterns. Please verify licenses and implementation details before using in production.

Need a custom component?

I build bespoke UI components & websites tailored to your brand.

DM me on X
PreviousMusic PlayerNextOrbit Card Stack
import { NewsletterBookshelf } from "@/components/ui/newsletter-bookshelf"
 
"use client"
 
import {
  NewsletterBookshelf,
  type NewsletterBookshelfItem,
} from "@/components/ui/newsletter-bookshelf"
 
const editions: NewsletterBookshelfItem[] = [
  {
    id: "edition-04",
    title: "The useful systems issue",
    date: "JUL 24, 2026",
    href: "/posts/useful-systems",
  },
  {
    id: "edition-03",
    title: "A better creative workflow",
    date: "JUL 17, 2026",
    href: "/posts/creative-workflow",
  },
  {
    id: "edition-02",
    title: "Notes on building in public",
    date: "JUL 10, 2026",
    href: "/posts/building-in-public",
    color: "#efe8d4",
  },
  {
    id: "edition-01",
    title: "The small team advantage",
    date: "JUL 3, 2026",
    href: "/posts/small-team-advantage",
    color: "#16277a",
  },
]
 
export default function Archive() {
  return (
    <NewsletterBookshelf
      items={editions}
      brand="Studio Notes"
      onSelect={(edition) => console.log("Selected", edition.id)}
    />
  )
}