← ui

poster-caption

ui

the lines under an open title: episode, time left, facts, its story

npx shadcn add @ag/poster-caption
Already installed? Preview an update
npx shadcn add @ag/poster-caption --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-11-05
  • New: the lines under an open title, <PosterCaption eyebrow headline facts synopsis />: an episode and the time left for a title started, its facts and its story for one not, the synopsis always holding the room of three lines. Its type is a share of its container's width.
Updated 2026-11-05 · release notes

shelf media

a title started

where it was left: the episode, then the time left

S3, E2 · Witness

49 min left

its facts, then its story, always the room of three lines

SeriesDrama202410 episodes12

In the countryside of the sixties, a young woman marries to save her family's farm, and finds the house she moves into full of locked rooms nobody will open, and a husband who keeps the keys.

the demo above, verbatim

poster-caption.demo.tsx
import { Sample } from "@/app/samples"
import { TitleFacts } from "@/registry/base-nova/ui/media-spec"
import { PosterCaption } from "@/registry/base-nova/ui/poster-caption"

export default function Demo() {
  return (
    <div className="space-y-12">
      <Sample
        name="started"
        glance
        title="a title started"
        description="where it was left: the episode, then the time left"
      >
        {/* Its type is a share of its container's width: a page's here. */}
        <div className="@container w-[60rem] max-w-full">
          <PosterCaption eyebrow="S3, E2 · Witness" headline="49 min left" />
        </div>
      </Sample>
      <Sample
        name="new"
        title="a title not started"
        description="its facts, then its story, always the room of three lines"
      >
        <div className="@container w-[60rem] max-w-full">
          <PosterCaption
            className="w-1/2"
            facts={
              <TitleFacts
                facts={["Series", "Drama", "2024", "10 episodes"]}
                age={12}
              />
            }
            synopsis="In the countryside of the sixties, a young woman marries to save her family's farm, and finds the house she moves into full of locked rooms nobody will open, and a husband who keeps the keys."
          />
        </div>
      </Sample>
    </div>
  )
}

the source, every file an install writes

poster-caption.tsx
import type * as React from "react"
import { cn } from "@/lib/utils"

// WHAT IS UNDER AN OPEN CARD: the lines a browse row shows beneath the title it is on.
// Up to four, each optional, in this order: a quiet line naming it ("S3, E2 · Witness",
// or the title itself), a lead line ("49 min left"), the facts (media-spec's
// `TitleFacts`: kind, genre, year, length, age), and a synopsis that always holds the
// room of three lines, so the shelf under it stands where it stands whatever the
// synopsis says. In the text's colour at the registry's tiers.
//
// Its type and lines are shares of the width of the container it is in (a shelf, as
// wide as its page), as measured on a TV's browse row: a line every 21 px of 1087 for
// the lead lines (cap heights 10 and 11), 19.5 for the synopsis (cap 11.5), 7 between.

const LINE =
  "h-[1.93cqw] truncate leading-[1.93cqw] text-[1.38cqw] text-foreground/93"

export function PosterCaption({
  eyebrow,
  headline,
  facts,
  synopsis,
  className,
}: {
  eyebrow?: React.ReactNode
  headline?: React.ReactNode
  facts?: React.ReactNode
  synopsis?: React.ReactNode
  className?: string
}) {
  const leads = Boolean(eyebrow || headline || facts)
  return (
    <div
      data-slot="poster-caption"
      className={cn("flex flex-col text-foreground", className)}
    >
      {eyebrow && (
        <p
          data-slot="poster-caption-eyebrow"
          className={cn(LINE, "font-medium text-[1.26cqw] text-foreground/61")}
        >
          {eyebrow}
        </p>
      )}
      {headline && (
        <p
          data-slot="poster-caption-headline"
          className={cn(LINE, "font-medium")}
        >
          {headline}
        </p>
      )}
      {facts && (
        <div
          data-slot="poster-caption-facts"
          className={cn(LINE, "flex items-center font-semibold")}
        >
          {facts}
        </div>
      )}
      {synopsis && (
        <p
          data-slot="poster-caption-synopsis"
          className={cn(
            "line-clamp-3 min-h-[5.382cqw] text-[1.45cqw] text-foreground/86 leading-[1.794cqw]",
            leads && "mt-[0.65cqw]",
          )}
        >
          {synopsis}
        </p>
      )}
    </div>
  )
}