Skip to content
← Blocks

Poster Hero

Poster-scale typography and a compact programme introduction in citrus and ink.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/poster-hero.json

Use it

Scroll horizontally for long lines
"use client";
import { PosterHero } from "@/components/jez-ui/blocks/poster-hero";

export default function Example() {
  return (
    <>
      <PosterHero />
    </>
  );
}

Compose your own

These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.

Scroll horizontally for long lines
"use client";
import {
  PosterHero,
  PosterHeroContent,
  PosterHeroBrand,
  PosterHeroCopyContent,
  PosterHeroDescription,
  PosterHeroAction,
} from "@/components/jez-ui/blocks/poster-hero";
export default function Example() {
  return (
    <PosterHero>
      <PosterHeroContent>
        <PosterHeroBrand />
        <PosterHeroCopyContent />
        <div className="grid gap-7 md:grid-cols-2">
          <PosterHeroDescription />
          <PosterHeroAction />
        </div>
      </PosterHeroContent>
    </PosterHero>
  );
}
Scroll horizontally for long lines
import { PosterHero, PosterHeroContent, PosterHeroTitle, PosterHeroLayout, PosterHeroMeta, PosterHeroBrand, PosterHeroAction, PosterHeroDescription, PosterHeroCopyContent } from "@/components/jez-ui/blocks/poster-hero";
Read the composition guide →

Props & types

Scroll horizontally for long lines
export type PosterHeroOptions = Pick<
  HeroProps,
  "copy" | "title" | "actionLabel" | "description" | "className" | "href"
>;

export type PosterHeroProps = Omit<
  React.ComponentProps<"section">,
  keyof PosterHeroOptions
> &
  PosterHeroOptions;

PosterHeroProps

Source

View 3 source files

registry/blocks/poster-hero.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "../ui/utils";
import { HeroLink, type HeroProps } from "./hero-parts";
export const PosterHeroCopy = {
  brand: "ASSEMBLY / A GATHERING OF IDEAS",
  meta: "ONLINE & EVERYWHERE",
  tagline: "Design. Culture.",
  taglineEnd: "Whatever comes next.",
};
export type PosterHeroOptions = Pick<
  HeroProps,
  "copy" | "title" | "actionLabel" | "description" | "className" | "href"
>;
export type PosterHeroProps = Omit<
  React.ComponentProps<"section">,
  keyof PosterHeroOptions
> &
  PosterHeroOptions;
function usePosterHeroModel({
  copy = {},
  title,
  actionLabel,
  description,
  className,
  href = "/blocks",
  children,
  ...rootProps
}: PosterHeroProps) {
  return {
    copy,
    title,
    actionLabel,
    description,
    className,
    href,
    children,
    rootProps,
  };
}
const PosterHeroCompositionContext = React.createContext<ReturnType<
  typeof usePosterHeroModel
> | null>(null);
function usePosterHeroComposition() {
  const context = React.useContext(PosterHeroCompositionContext);
  if (!context) throw new Error("PosterHero parts must be inside PosterHero.");
  return context;
}
export function PosterHero(props: PosterHeroProps) {
  const model = usePosterHeroModel(props);
  const { className, rootProps, children } = model;
  return (
    <PosterHeroCompositionContext.Provider value={model}>
      <section
        {...rootProps}
        className={cn(
          "relative isolate overflow-hidden rounded-xl bg-[#dfeb5a] text-[#243022]",
          className,
        )}
      >
        {children !== undefined ? children : <PosterHeroLayout />}
      </section>
    </PosterHeroCompositionContext.Provider>
  );
}

export function PosterHeroContent({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="poster-hero-content"
      className={cn("p-7 md:p-12", className)}
      {...props}
    />
  );
}
export function PosterHeroTitle({
  className,
  ...props
}: React.ComponentProps<"h1">) {
  return (
    <h1
      data-slot="poster-hero-title"
      className={cn(
        "font-display text-[clamp(3.5rem,10vw,8rem)] font-bold leading-[.86] tracking-[-.065em]",
        className,
      )}
      {...props}
    />
  );
}

export function PosterHeroLayout({
  children,
  ...props
}: Partial<React.ComponentProps<typeof PosterHeroContent>> & {
  children?: React.ReactNode;
}) {
  const { copy } = usePosterHeroComposition();
  return (
    <PosterHeroContent {...props}>
      {children === undefined ? (
        <>
          <div className="flex justify-between text-xs font-medium">
            <PosterHeroBrand />
            <PosterHeroMeta />
          </div>
          <PosterHeroCopyContent />
          <div className="grid gap-7 md:grid-cols-[1fr_1fr_auto]">
            <p className="text-sm font-medium">
              {copy.tagline ?? "Design. Culture."}
              <br />
              {copy.taglineEnd ?? "Whatever comes next."}
            </p>
            <PosterHeroDescription />
            <div>
              <PosterHeroAction />
            </div>
          </div>
        </>
      ) : (
        children
      )}
    </PosterHeroContent>
  );
}

export function PosterHeroMeta({
  children,
  ...props
}: Partial<React.ComponentProps<"span">> & { children?: React.ReactNode }) {
  const { copy } = usePosterHeroComposition();
  return (
    <span {...props}>
      {children === undefined ? (copy.meta ?? "ONLINE & EVERYWHERE") : children}
    </span>
  );
}
export function PosterHeroBrand({
  children,
  ...props
}: Partial<React.ComponentProps<"span">> & { children?: React.ReactNode }) {
  const { copy } = usePosterHeroComposition();
  return (
    <span {...props}>
      {children === undefined
        ? (copy.brand ?? "ASSEMBLY / A GATHERING OF IDEAS")
        : children}
    </span>
  );
}
export function PosterHeroAction({
  children,
  ...props
}: Partial<React.ComponentProps<typeof HeroLink>> & {
  children?: React.ReactNode;
}) {
  const { actionLabel, href } = usePosterHeroComposition();
  return (
    <HeroLink href={href} {...props}>
      {children === undefined ? (actionLabel ?? "View programme") : children}
    </HeroLink>
  );
}
export function PosterHeroDescription({
  children,
  ...props
}: Partial<React.ComponentProps<"p">> & { children?: React.ReactNode }) {
  const { description } = usePosterHeroComposition();
  return (
    <p
      {...props}
      className={cn("max-w-xs text-sm leading-relaxed", props.className)}
    >
      {children === undefined
        ? (description ??
          "Conversations for curious people. A programme built around sharing what we know and asking what we don’t.")
        : children}
    </p>
  );
}
export function PosterHeroCopyContent({
  children,
  ...props
}: Partial<React.ComponentProps<"div">> & { children?: React.ReactNode }) {
  const { title } = usePosterHeroComposition();
  return (
    <div
      {...props}
      className={cn("my-12 border-y-2 border-[#243022] py-6", props.className)}
    >
      {children === undefined ? (
        <PosterHeroTitle>
          {title ?? (
            <>
              COME WITH
              <br />
              QUESTIONS.
              <br />
              <span className="font-serif font-normal italic tracking-tight">
                Leave inspired.
              </span>
            </>
          )}
        </PosterHeroTitle>
      ) : (
        children
      )}
    </div>
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

registry/blocks/hero-parts.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { Slot } from "radix-ui";
import { ArrowUpRight } from "lucide-react";
import { cn } from "../ui/utils";
export type HeroCopy = Partial<
  Record<
    | "brand"
    | "meta"
    | "eyebrow"
    | "tagline"
    | "taglineEnd"
    | "caption"
    | "footerNote"
    | "animationName"
    | "playLabel"
    | "pauseLabel"
    | "artworkLabel",
    string
  >
>;
export type HeroProps = {
  copy?: HeroCopy;
  preview?: React.ReactNode;
  artworkText?: string;
  className?: string;
  href?: string;
  title?: React.ReactNode;
  description?: React.ReactNode;
  actionLabel?: React.ReactNode;
  secondaryImageSrc?: string;
  secondaryImageAlt?: string;
  imageSrc?: string;
  imageAlt?: string;
  artwork?: {
    color?: string;
    speed?: number;
    label?: string;
    playLabel?: string;
    pauseLabel?: string;
  };
};
export function HeroLink({
  href = "/blocks",
  asChild,
  children = "Explore the collection",
  className,
  ...props
}: React.ComponentProps<"a"> & {
  asChild?: boolean;
  href?: string;
  children?: React.ReactNode;
  className?: string;
}) {
  const Comp = asChild ? Slot.Root : "a";
  return (
    <Comp
      {...props}
      href={href}
      className={cn(
        "inline-flex items-center gap-3 rounded-full border border-current/30 px-5 py-3 text-sm transition-colors hover:bg-current/10",
        className,
      )}
    >
      {asChild ? (
        children
      ) : (
        <>
          {children}
          <ArrowUpRight size={16} />
        </>
      )}
    </Comp>
  );
}

Dependencies

clsx@2.1.1 · tailwind-merge@3.5.0 · radix-ui@1.6.7 · lucide-react@0.577.0

Accessibility & behaviour

Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.

Read the accessibility and performance guide →