a genre or a collection as a lockup on a dark tile, lighter when open
npx shadcn add @ag/category-tileAlready installed? Preview an update
npx shadcn add @ag/category-tile --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-11-05
- New: a genre or a collection's lockup on a tile of dark glass, lighter when open:
<CategoryTile open>{lockup}</CategoryTile>. A row of them is aShelfwithlayout="tiles"andhideTitle, which rings the open one.
the demo above, verbatim
"use client"
import { CATEGORIES, category } from "@/app/browse-titles"
import { Sample } from "@/app/samples"
import { CategoryTile } from "@/registry/base-nova/ui/category-tile"
import { Shelf } from "@/registry/base-nova/ui/shelf"
export default function Demo() {
return (
<div className="space-y-12">
<Sample name="tiles" glance title="one open, one at rest">
<div className="dark flex gap-3 bg-black p-4 text-white">
<CategoryTile open height="4.5rem">
{category("dramas").lockup}
</CategoryTile>
<CategoryTile open={false} height="4.5rem">
{category("thrillers").lockup}
</CategoryTile>
</div>
</Sample>
<Sample
name="row"
title="a row of categories"
description="a shelf of tiles, its title hidden; ← → once the row has focus"
>
<div className="dark @container -mx-4 bg-black text-white">
<Shelf
label="Categories"
hideTitle
layout="tiles"
items={CATEGORIES}
itemKey={(c) => c.id}
itemLabel={(c) => c.label}
>
{(c, { open }) => (
<CategoryTile open={open}>{c.lockup}</CategoryTile>
)}
</Shelf>
</div>
</Sample>
</div>
)
}the source, every file an install writes
import type * as React from "react"
import { cn } from "@/lib/utils"
import { CARD_LAYER } from "@/registry/base-nova/lib/shelf-motion"
// A CATEGORY AS A TILE: a genre or a collection's lockup (its name set as type, a
// mark) centred on a tile of dark glass, the way a browse page lines its categories up
// under the billboard (163 by 82 px of 1087 on a TV, its corner 9). Open, its glass is
// lighter (35 to 17 at rest, 53 to 31 open, a lit top edge on both); the ring is its
// shelf's. A row of them is a `shelf` of layout "tiles" with its title hidden. It is a
// drawing: what it is pressed as is its shelf's. Its size is its box's, or `height`
// alone (its width twice that); the lockup is a share of its height.
export function CategoryTile({
open,
children,
height,
className,
}: {
open: boolean
/** The lockup: words styled as the category's own type, or a mark. */
children: React.ReactNode
/** Its height alone, outside a shelf (any CSS length). */
height?: string
className?: string
}) {
// The box is the container its insides measure (a container's own lengths would
// read the one above it).
return (
<div
data-slot="category-tile"
data-open={open || undefined}
className={cn(
"relative [container-type:size]",
height ? "aspect-[1.9877] h-(--tile-h)" : "size-full",
className,
)}
style={
height ? ({ "--tile-h": height } as React.CSSProperties) : undefined
}
>
<div
className={cn(
"absolute inset-0 grid place-items-center overflow-hidden rounded-[10.98cqh] bg-linear-to-b px-[16cqh] text-center font-black text-[27cqh] text-white leading-none shadow-[inset_0_1px_0_rgb(255_255_255/0.36)]",
open ? "from-white/21 to-white/11" : "from-white/14 to-white/7",
// The whole tile is the picture a shelf's step fades and moves.
CARD_LAYER.art,
)}
>
{children}
</div>
</div>
)
}