The top of a page: a headline with an optional line above it (eyebrow) and a status beside that (badge), a trail above them (breadcrumb), a lede under the headline (description), the page's buttons (actions), more such as a byline (children), and a big picture, or something live in its place (media), below. The picture is light and dark art, swapped with the page's dark mode. The picture rises into place when it first appears unless motion="none", and it is never shown wider than its intrinsic width. Its frame has 24px squircle corners, Ouro's corner shape.
On this site
This site's own pages use Hero too. Open one with X-ray pointing it out: The home page
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.68Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/hero#v1.3.68Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/heroIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/hero#v1.3.68 ourostack/ouro-ui/hero-tests#v1.3.68import { Hero } from "@/components/ouro/hero/hero";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<Hero
eyebrow="Recipes"
title="Dinner, sorted"
description="Plan the week, shop once, cook without the scramble."
actions={<Button>Start planning</Button>}
picture={{
light: { src: "/hero.jpg", width: 1536, height: 576, alt: "A table set for four" },
dark: { src: "/hero-night.jpg", width: 1536, height: 576 },
}}
/>Use instead when
- Use
PageHeaderinstead for a page's top with no picture, andHeadingfor a plain title with nothing around it.
Props
Type: HeroProps.
actions- Type:
ReactNode - The hero's buttons or links, such as Ouro buttons. They sit right under the lede, its next element, on the lede's edge, set a little further from it than the lines of text are from each other. Pass the page's main button here, not in
description. badge- Type:
ReactNode - A short status beside the eyebrow, such as a
Badge:badge={<Badge>Beta</Badge>}. Hero sets it on the eyebrow's line, or on that line alone without an eyebrow, and leaves its look to what you pass. breadcrumb- Type:
ReactNode - A trail above everything else, such as a
Breadcrumb, on the text's edge. children- Type:
ReactNode - More under the actions, in the text column, such as a byline, a note or a short recording. In the split layout it sits beside the picture. A second picture or a demo belongs in the next section, not the hero.
description- Type:
ReactNode - The lede: a short paragraph under the title. Text is set in a
<p>; anything else, such as paragraphs or a list, is set in a<div>so it can hold blocks, and Hero clears its first block's top margin and its last block's bottom margin, so a paragraph needs nom-0. It runs the full width of the hero's text column, like the title. Its lines are balanced to even lengths in every layout, so the last line is never left short; a lede longer than the browser balances, a few lines, wraps as usual. To choose where a lede breaks, set each phrase in its owninline-blockspan, as theLongLedestory does. eyebrow- Type:
ReactNode - A short line above the title that says what kind of page this is.
fullWidth- Type:
boolean - Stretches the hero to the width of its container, with no gutter or cap of its own, for a hero inside a page column that already sets the width. Without it the hero keeps a gutter at either side and caps its width.
headingLevel- Type:
1 | 2 - Default:
1 - The title's heading level:
1for the page's headline,2when the hero heads a section. layout- Type:
"stage" | "column" | "split" - Default:
"stage" stagecentres the text over the picture;columnaligns both to a reading column;splitputs the copy and the media side by side from 64rem, and stacks them likecolumnbelow that, for a page whose picture or live media belongs next to its words.media- Type:
ReactNode - Something live in the picture's place, such as a video, a demo or a scene. It rises in like a picture and fills the hero's width, up to
mediaWidth. Usepicturefor a still image. mediaWidth- Type:
number - The widest the live
mediais shown, in pixels, such as a scene image's intrinsic width, so it is never upscaled. Without it the media fills the hero's width. Apictureuses its ownwidthinstead. motion- Type:
"rise" | "none" - Default:
"rise" riselifts the picture into place when it first appears, with reduced motion fading it in. The rise is a CSS transition from@starting-stylein Hero's own classes, so it plays from server-rendered HTML before any JavaScript runs, and it needs no keyframes and no stylesheet to import. It takes its duration and easing fromouro-base's--ouro-duration-slow,--ouro-duration-fastand--ouro-ease-out. The wrapper carriesdata-starting-styleonly while a hydrated page sees the rise running, so server HTML that never hydrates, such as an Astro page without client JavaScript, carries no marker.picture- Type:
HeroPictures - The big picture, shown under the text:
{ light, dark }, the same scene by day and at night, swapped with the page's dark mode. Both are required: a hero has night art. Once the picture shown has loaded, the media frame carriesdata-loaded, so a site or a test can wait on[data-slot="media"][data-loaded]. A small inline script in the server's HTML sets it before any JavaScript bundle runs, and Hero sets it itself after hydration or a client render, so it also arrives where a content security policy blocks inline scripts. ref- Type:
Ref<HTMLElement> - Reaches the rendered element.
title(required)- Type:
ReactNode - The hero's headline, its
<h1>(or<h2>withheadingLevel={2}). Required.
Native attributes
Also accepts the attributes in React's HTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
action: Not a prop. The buttons areactions:<Hero actions={<Button>Start</Button>} />.heading: Not a prop. The headline istitle:<Hero title="Dinner, sorted" />.image: Not a prop. The picture ispicture:<Hero picture={{ light: { src, width, height, alt }, dark: { src, width, height } }} />.overline: Not a prop. The line above the title iseyebrow:<Hero eyebrow="Recipes" />.subtitle: Not a prop. The lede isdescription:<Hero description="…" />.
Types
HeroPicture
The hero's picture. It never shows wider than its intrinsic width.
interface HeroPicture {
/** The fallback `<img>` file. */
src: string;
/**
* The intrinsic pixel width of the wide file. The picture is never shown wider than this.
*/
width: number;
/** The intrinsic pixel height, which sets the picture's aspect ratio and stops layout shift. */
height: number;
/** What the picture shows. Required unless `decorative` is set. */
alt?: string;
/** Hides the picture from assistive technology, with `alt=""`. */
decorative?: boolean;
/** `<source>` elements, in order: for example phone formats with a `media` query, then wide formats. */
sources?: HeroPictureSource[];
/** The `<img>`'s `srcset`. */
srcSet?: string;
/** The `<img>`'s `sizes`. */
sizes?: string;
/** Loads the picture early with `fetchPriority="high"`. Otherwise it loads lazily. In `{ light, dark }`, see `HeroPictures` for how each twin loads. */
priority?: boolean;
/**
* A wide aspect ratio, such as `"8 / 3"`, applied on wider screens in the stage and column
* layouts. Without it the picture keeps its own ratio, as it does on a phone and in the split
* layout, so a wide crop never cuts the subject in the split layout's narrower column.
*/
crop?: string;
/**
* The focal point kept in view when the picture is cropped, as an `object-position`, such as
* `"50% 80%"` for a subject near the bottom. Set it on each picture: the dark one takes the
* light one's unless it has its own.
*
* @default "50% 42%"
*/
focus?: string;
/**
* A colour painted in the picture's box while the image loads, such as the art's main colour,
* in place of an empty hole. It fills the box the `width` and `height` already reserve, so it
* moves nothing, and an opaque image covers it once it decodes. Give the dark picture its own,
* as it never takes the light one's: a day colour would flash at night. Hero has the file's
* address but not its pixels, so take the colour from your image pipeline, for example the
* `dominant` colour of `sharp(file).stats()`, or pick it by eye. Leave it out for art with
* transparency, which would show it through.
*/
placeholder?: string;
}HeroPictures
The hero's art in light and dark mode: the same scene by day and at night. Hero shows light or dark in CSS, with your app's Tailwind dark variant, the one every Ouro component's dark styles follow, so the right one is there from the first paint. That variant reads the .dark class Ouro's colorSchemeScript sets; a site that marks dark mode with data-theme or the system setting redefines it, as the theming guide's dark mode recipe shows, and Hero follows. dark shares light's frame: its width and crop size the box. It is the same scene, so without its own alt, decorative or focus it takes light's. priority decides how the twin that shows loads. A twin without it is lazy, and a hidden lazy image is never fetched. Give priority to the twin most of your visitors see, and it loads straight from the HTML at high fetch priority while the other stays lazy. Give it to both when the page preloads the art for its scheme: both wait in the HTML, and an inline script after them, which runs once the page's CSS has loaded, loads the one that shows, so neither scheme downloads both. A preload for the dark twin, which follows the system setting, is <link rel="preload" as="image" fetchpriority="high" media="(prefers-color-scheme: dark)" imagesrcset="/night-960.avif 960w, /night.avif 1920w" imagesizes="100vw">, with exactly the twin's srcSet and sizes (and a type for a sources format); a page that stores the person's choice adds the link from the script in its <head> that applies the choice.
interface HeroPictures {
/** The art shown in light mode, and the frame both are shown in. */
light: HeroPicture;
/** The same scene at night, shown in dark mode. */
dark: HeroPicture;
}HeroPictureSource
One <source> element of a hero picture, in the order given.
interface HeroPictureSource {
/** The source's `srcset`, such as an AVIF or WebP file. */
srcSet: string;
/** The MIME type, such as `image/avif`. */
type?: string;
/** A media query that picks this source, such as `(max-width: 40rem)`. */
media?: string;
/** The `sizes` for this source. */
sizes?: string;
/**
* This source's intrinsic pixel width, when its art has another shape than the picture's own,
* such as a phone crop. With `height`, it holds the image's box to this source's shape before
* the file arrives, so the page doesn't shift.
*/
width?: number;
/** This source's intrinsic pixel height, with `width`. */
height?: number;
}
