Journal Hero
An editorial cover, serif headline, and spacious introduction to a publication.
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/journal-hero.jsonUse it
Scroll horizontally for long lines
"use client";
import { JournalHero } from "@/components/jez-ui/blocks/journal-hero";
export default function Example() {
return (
<>
<JournalHero />
</>
);
}
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 {
JournalHero,
JournalHeroMasthead,
JournalHeroContent,
JournalHeroCopyContent,
JournalHeroMedia,
} from "@/components/jez-ui/blocks/journal-hero";
export default function Example() {
return (
<JournalHero>
<JournalHeroMasthead />
<JournalHeroContent>
<JournalHeroCopyContent />
<JournalHeroMedia />
</JournalHeroContent>
</JournalHero>
);
}
Scroll horizontally for long lines
import { JournalHero, JournalHeroHeader, JournalHeroContent, JournalHeroTitle, JournalHeroMasthead, JournalHeroLayout, JournalHeroBrand, JournalHeroAction, JournalHeroEyebrow, JournalHeroMedia, JournalHeroDescription, JournalHeroCopyContent } from "@/components/jez-ui/blocks/journal-hero";Props & types
Scroll horizontally for long lines
export type JournalHeroOptions = Pick<
HeroProps,
| "copy"
| "title"
| "actionLabel"
| "description"
| "imageSrc"
| "imageAlt"
| "className"
| "href"
>;
export type JournalHeroProps = Omit<
React.ComponentProps<"section">,
keyof JournalHeroOptions
> &
JournalHeroOptions;
JournalHeroPropsSource
View 4 source files
registry/blocks/journal-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 JournalHeroCopy = {
brand: "Still.",
tagline: "A journal for paying attention",
eyebrow: "From the editor / Issue 04",
caption: "A slower internet. A wider view.",
};
export type JournalHeroOptions = Pick<
HeroProps,
| "copy"
| "title"
| "actionLabel"
| "description"
| "imageSrc"
| "imageAlt"
| "className"
| "href"
>;
export type JournalHeroProps = Omit<
React.ComponentProps<"section">,
keyof JournalHeroOptions
> &
JournalHeroOptions;
function useJournalHeroModel({
copy = {},
title,
actionLabel,
description,
imageSrc,
imageAlt,
className,
href = "/blocks",
children,
...rootProps
}: JournalHeroProps) {
return {
copy,
title,
actionLabel,
description,
imageSrc,
imageAlt,
className,
href,
children,
rootProps,
};
}
const JournalHeroCompositionContext = React.createContext<ReturnType<
typeof useJournalHeroModel
> | null>(null);
function useJournalHeroComposition() {
const context = React.useContext(JournalHeroCompositionContext);
if (!context)
throw new Error("JournalHero parts must be inside JournalHero.");
return context;
}
export function JournalHero(props: JournalHeroProps) {
const model = useJournalHeroModel(props);
const { className, rootProps, children } = model;
return (
<JournalHeroCompositionContext.Provider value={model}>
<section
{...rootProps}
className={cn(
"relative isolate overflow-hidden rounded-xl bg-[#f6f0e7] text-[#43372d]",
className,
)}
>
{children !== undefined ? (
children
) : (
<>
<JournalHeroMasthead />
<JournalHeroLayout />
</>
)}
</section>
</JournalHeroCompositionContext.Provider>
);
}
export function JournalHeroHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="journal-hero-header"
className={cn(
"flex items-end justify-between border-b border-[#43372d]/25 p-7",
className,
)}
{...props}
/>
);
}
export function JournalHeroContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="journal-hero-content"
className={cn(
"grid gap-8 p-7 md:grid-cols-[1fr_1.2fr] md:p-10",
className,
)}
{...props}
/>
);
}
export function JournalHeroTitle({
className,
...props
}: React.ComponentProps<"h1">) {
return (
<h1
data-slot="journal-hero-title"
className={cn(
"font-serif text-5xl leading-[1.05] tracking-tight md:text-6xl",
className,
)}
{...props}
/>
);
}
export function JournalHeroMasthead({
children,
...props
}: Partial<React.ComponentProps<typeof JournalHeroHeader>> & {
children?: React.ReactNode;
}) {
const { copy } = useJournalHeroComposition();
return (
<JournalHeroHeader {...props}>
{children === undefined ? (
<>
<JournalHeroBrand />
<span className="text-xs">
{copy.tagline ?? "A journal for paying attention"}
</span>
</>
) : (
children
)}
</JournalHeroHeader>
);
}
export function JournalHeroLayout({
children,
...props
}: Partial<React.ComponentProps<typeof JournalHeroContent>> & {
children?: React.ReactNode;
}) {
const { copy } = useJournalHeroComposition();
return (
<JournalHeroContent {...props}>
{children === undefined ? (
<>
<JournalHeroCopyContent />
<figure>
<JournalHeroMedia />
<figcaption className="mt-3 text-xs text-[#43372d]/65">
{copy.caption ?? "A slower internet. A wider view."}
</figcaption>
</figure>
</>
) : (
children
)}
</JournalHeroContent>
);
}
export function JournalHeroBrand({
children,
...props
}: Partial<React.ComponentProps<"span">> & { children?: React.ReactNode }) {
const { copy } = useJournalHeroComposition();
return (
<span {...props} className={cn("font-serif text-4xl", props.className)}>
{children === undefined ? (copy.brand ?? "Still.") : children}
</span>
);
}
export function JournalHeroAction({
children,
...props
}: Partial<React.ComponentProps<typeof HeroLink>> & {
children?: React.ReactNode;
}) {
const { actionLabel, href } = useJournalHeroComposition();
return (
<HeroLink href={href} {...props}>
{children === undefined
? (actionLabel ?? "Read the latest issue")
: children}
</HeroLink>
);
}
export function JournalHeroEyebrow({
children,
...props
}: Partial<React.ComponentProps<"p">> & { children?: React.ReactNode }) {
const { copy } = useJournalHeroComposition();
return (
<p
{...props}
className={cn("text-xs uppercase tracking-widest", props.className)}
>
{children === undefined
? (copy.eyebrow ?? "From the editor / Issue 04")
: children}
</p>
);
}
export function JournalHeroMedia({
children,
...props
}: Partial<React.ComponentProps<"img">> & { children?: React.ReactNode }) {
const { imageSrc, imageAlt } = useJournalHeroComposition();
return children === undefined ? (
<img
src={imageSrc ?? "/assets/editorial-slow.svg"}
alt={imageAlt ?? "Abstract editorial study in warm geometric forms"}
{...props}
className={cn("aspect-[4/5] w-full object-cover", props.className)}
/>
) : (
children
);
}
export function JournalHeroDescription({
children,
...props
}: Partial<React.ComponentProps<"p">> & { children?: React.ReactNode }) {
const { description } = useJournalHeroComposition();
return (
<p
{...props}
className={cn("max-w-xs text-sm leading-relaxed", props.className)}
>
{children === undefined
? (description ??
"On small observations, everyday objects, and the things we miss when we move too quickly.")
: children}
</p>
);
}
export function JournalHeroCopyContent({
children,
...props
}: Partial<React.ComponentProps<"div">> & { children?: React.ReactNode }) {
const { title } = useJournalHeroComposition();
return (
<div
{...props}
className={cn(
"flex flex-col items-start justify-between gap-8",
props.className,
)}
>
{children === undefined ? (
<>
<JournalHeroEyebrow />
<JournalHeroTitle>
{title ?? (
<>
The art of
<br />
<em>noticing.</em>
</>
)}
</JournalHeroTitle>
<JournalHeroDescription />
<JournalHeroAction />
</>
) : (
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>
);
}
assets/editorial-slow.svg
Scroll horizontally for long lines
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 900"><rect width="1600" height="900" fill="#d8dbcc"/><rect x="160" y="120" width="1280" height="660" rx="16" fill="#f4efe1" stroke="#32483c" stroke-width="3"/><path d="M160 200H1440" stroke="#32483c" stroke-width="3"/><g fill="#a76245"><circle cx="205" cy="160" r="9"/><circle cx="239" cy="160" r="9"/><circle cx="273" cy="160" r="9"/></g><path d="M250 685V470a230 230 0 01460 0v215z" fill="#32483c"/><path d="M860 685V420a230 230 0 01460 0v265z" fill="#c68c66"/><g fill="none" stroke="#f4efe1" stroke-width="3"><path d="M280 655C440 280 550 290 680 630S980 930 1160 330"/><path d="M290 680C450 305 560 315 690 655S990 955 1170 355"/><path d="M300 705C460 330 570 340 700 680S1000 980 1180 380"/></g><circle cx="1090" cy="435" r="96" fill="#f4efe1"/><path d="M1035 381v118l34-34 25 48 24-13-27-45h53z" fill="#32483c"/><path d="M350 732H1250" stroke="#32483c" stroke-width="2"/><circle cx="790" cy="160" r="8" fill="#32483c"/></svg>
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 →