Hero

The opening section of a landing page

Hero is the opening section of a landing page: an announcement, a headline, a line of supporting text and calls to action, optionally beside an image. Centered and stacked by default; split side by side on wider screens. The home page opens with one.

Installation

Install the dependencies.

npm install @kushagradhawan/kookie-ui

Copy this file into components/blocks/hero. Keep the folder names, since blocks import each other by relative path.

components/blocks/hero/hero.tsx
import * as React from 'react';
import { Flex, Heading, Text } from '@kushagradhawan/kookie-ui';
import type { FlexProps, HeadingProps, TextProps } from '@kushagradhawan/kookie-ui';

type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
type Responsive<T> = T | Partial<Record<Breakpoint, T>>;
type Layout = 'stacked' | 'split';

function layoutToDirection(layout: Responsive<Layout>): Responsive<'row' | 'column'> {
  const toDirection = (value: Layout) => (value === 'split' ? 'row' : 'column');
  if (typeof layout === 'string') return toDirection(layout);

  const result: Partial<Record<Breakpoint, 'row' | 'column'>> = {};
  for (const [breakpoint, value] of Object.entries(layout) as [Breakpoint, Layout | undefined][]) {
    if (value) result[breakpoint] = toDirection(value);
  }
  return result;
}

type HeroRootProps = FlexProps & {
  /** `stacked` centers everything in a column; `split` places media beside the copy. Responsive. */
  layout?: Responsive<Layout>;
};

const HeroRoot = React.forwardRef<HTMLDivElement, HeroRootProps>(
  ({ layout = 'stacked', align = 'center', gap = '6', direction, ...props }, ref) => (
    <Flex ref={ref} direction={direction ?? layoutToDirection(layout)} align={align} gap={gap} {...props} />
  ),
);
HeroRoot.displayName = 'Hero.Root';

/** Logos, badges or announcements above the title. */
const HeroMeta = React.forwardRef<HTMLDivElement, FlexProps>(({ gap = '2', align = 'center', ...props }, ref) => (
  <Flex ref={ref} gap={gap} align={align} {...props} />
));
HeroMeta.displayName = 'Hero.Meta';

const HeroTitle = React.forwardRef<HTMLHeadingElement, HeadingProps>(
  ({ size = '9', weight = 'medium', align = 'center', ...props }, ref) => (
    <Heading ref={ref} size={size} weight={weight} align={align} {...props} />
  ),
);
HeroTitle.displayName = 'Hero.Title';

const HeroDescription = React.forwardRef<HTMLParagraphElement, TextProps>(
  ({ size = '4', align = 'center', ...props }, ref) => <Text ref={ref} size={size} align={align} {...props} />,
);
HeroDescription.displayName = 'Hero.Description';

const HeroActions = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ gap = '2', direction = 'row', align = 'center', ...props }, ref) => (
    <Flex ref={ref} gap={gap} direction={direction} align={align} {...props} />
  ),
);
HeroActions.displayName = 'Hero.Actions';

/** Image, video or product shot. */
const HeroMedia = React.forwardRef<HTMLDivElement, FlexProps>((props, ref) => <Flex ref={ref} {...props} />);
HeroMedia.displayName = 'Hero.Media';

export const Hero = Object.assign(HeroRoot, {
  Root: HeroRoot,
  Meta: HeroMeta,
  Title: HeroTitle,
  Description: HeroDescription,
  Actions: HeroActions,
  Media: HeroMedia,
});

Usage

import { Hero } from '@/components/blocks/hero/hero';
 
<Hero.Root>
  <Hero.Title>Ship interfaces faster</Hero.Title>
  <Hero.Description>Accessible components with a design system built in.</Hero.Description>
  <Hero.Actions>
    <Button size="3" variant="classic">Get started</Button>
  </Hero.Actions>
</Hero.Root>

Anatomy

<Hero.Root>
  <Hero.Meta />
  <Hero.Title />
  <Hero.Description />
  <Hero.Actions />
  <Hero.Media />
</Hero.Root>

Props

Every part accepts the props of the component it wraps. Only the additions and changed defaults are listed.

PartWrapsAdds or changes
RootFlexlayout?: Responsive<'stacked' | 'split'> ('stacked'). align="center", gap="6"
MetaFlexgap="2", align="center"
TitleHeadingsize="9", weight="medium", align="center"
DescriptionTextsize="4", align="center"
ActionsFlexdirection="row", gap="2", align="center"
MediaFlex-

stacked sets direction="column"; split sets direction="row". Pass direction to override either.

Title renders an h1 by default through Heading. Keep one per page.

Examples

Product Launch

New in 2.0

Ship interfaces faster

Accessible components with a design system built in.
<Hero.Root>
  <Hero.Meta>
    <Badge variant="soft" color="gray">New in 2.0</Badge>
  </Hero.Meta>
  <Hero.Title size={{ initial: '7', sm: '8' }}>Ship interfaces faster</Hero.Title>
  <Hero.Description size="3" emphasis="medium">
    Accessible components with a design system built in.
  </Hero.Description>
  <Hero.Actions>
    <Button size="3" variant="soft" color="gray">Read the docs</Button>
    <Button size="3" variant="classic">Get started</Button>
  </Hero.Actions>
</Hero.Root>

Split With Media

Copy on the left, product shot on the right from md up. Align the text to the start when splitting.

Plan less. Build more.

Roadmaps, issues and releases in one place.
Sprint 1412 of 18 issues done
<Hero.Root
  layout={{ initial: 'stacked', md: 'split' }}
  align={{ initial: 'start', md: 'center' }}
>
  <Flex direction="column" gap="4" flexGrow="1" flexBasis="0">
    <Hero.Title size="7" align="left">Plan less. Build more.</Hero.Title>
    <Hero.Description size="3" emphasis="medium" align="left">
      Roadmaps, issues and releases in one place.
    </Hero.Description>
    <Hero.Actions>
      <Button size="3" variant="classic">Start free</Button>
    </Hero.Actions>
  </Flex>
  <Hero.Media flexGrow="1" flexBasis="0" width="100%">
    <Image src="/product.png" alt="The sprint board" width={640} height={400} />
  </Hero.Media>
</Hero.Root>

Accessibility

Give Media images meaningful alt text, or an empty alt when they are decorative. Use links (Button asChild around your router link) for actions that navigate.

© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background