Poster Hero
Poster-scale typography and a compact programme introduction in citrus and ink.
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.jsonUse 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";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;
PosterHeroPropsSource
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 →