Skip to content
Motion
FreeIn reviewv1.0.0

Editorial Hero

A magazine opening: a large serif headline, the intro and actions, tags, and the photo framed tall or set as a band.

npx shadcn@latest add @vibebox/hero-editorial

Controls

Variant
<HeroEditorial />

About

Rebuilds the site kit's editorial intro and text-only heroes. The serif headline gives a crafted, magazine feel for studios, food and hospitality. It differs from hero-kinetic-type (animated sans poster type) and hero-centered.

Use it when

  • The brand voice matters as much as the photo.
  • There is one strong portrait photo, or none yet.

Skip it when

  • The page needs a booking stage: use hero-split-stage.
  • The headline is a product claim for software: use hero-centered.

Install

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the block with the shadcn CLI:
    npx shadcn@latest add @vibebox/hero-editorial
  3. 3Use it:
    import { HeroEditorial } from "@/components/vibebox/hero-editorial";
    import { home } from "@/content/pages/home";
    
    <HeroEditorial content={home.hero} variant="framed" id="top" />

Code

2 files
components/vibebox/hero-editorial/index.tsx
/**
 * VibeBox Motion · Editorial Hero
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
import { Actions, Container, Eyebrow, headingId, Media, REVEAL, Section, SlotText, type BlockProps } from "@/lib/vibebox/block-kit";
import type { Image } from "@/lib/vibebox/content";
import { cn } from "@/lib/utils";
import type { Content, Variant } from "./schema";

/*
 * A magazine opening: a serif headline set large, the intro and actions, a row of tags, and the
 * photo either framed tall beside the copy or as a slim band under the type.
 */

const FALLBACK_MEDIA: Image = { src: "scene:window", alt: "", width: 1200, height: 1500 };

export type HeroEditorialProps = BlockProps<Content, Variant>;

export function HeroEditorial({ content, variant = "framed", id, tone, className }: HeroEditorialProps) {
  const framed = variant === "framed";
  return (
    <Section
      slug="hero-editorial"
      variant={variant}
      id={id}
      tone={tone}
      labelledBy={headingId(id)}
      spacing={false}
      className={cn("overflow-clip py-[clamp(48px,8vw,120px)]", className)}
    >
      <Container className={cn("grid grid-cols-[minmax(0,1fr)] gap-[clamp(36px,5vw,64px)]", framed && "lg:grid-cols-[minmax(0,1.3fr)_minmax(0,0.7fr)] lg:items-end")}>
        <div className="grid gap-6">
          <Eyebrow value={content.eyebrow} />
          <SlotText
            as="h1"
            value={content.heading}
            path="heading"
            id={headingId(id)}
            className={cn("font-serif font-normal tracking-[-0.035em] text-balance", framed ? "text-h1" : "text-hero")}
          />
          <div className={cn("grid gap-6", !framed && "lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end lg:gap-12")}>
            <SlotText as="p" value={content.lead} path="lead" className="max-w-[54ch] text-[clamp(1.05rem,1.3vw,1.25rem)] leading-relaxed text-pretty text-muted-foreground" />
            <Actions primary={content.primaryAction} secondary={content.secondaryAction} size="lg" />
          </div>
          {content.tags.length > 0 && (
            <ul className="flex flex-wrap gap-x-5 gap-y-2 border-t border-border pt-5 text-small font-medium text-muted-foreground">
              {content.tags.map((tag, index) => (
                <li key={index} className="flex items-center gap-2">
                  <span aria-hidden="true" className="size-1 rounded-full bg-spectrum-3" />
                  <span data-slot={`tags.${index}`}>{tag}</span>
                </li>
              ))}
            </ul>
          )}
        </div>
        <figure className={cn("m-0 grid gap-3", REVEAL)}>
          <div className={cn("overflow-hidden rounded-xl ring-1 ring-border", framed ? "aspect-[4/5]" : "aspect-[16/7] sm:aspect-[21/7]")}>
            <Media image={content.media ?? FALLBACK_MEDIA} priority sizes={framed ? "(min-width: 1024px) 35vw, 100vw" : "100vw"} />
          </div>
          <SlotText as="figcaption" value={content.caption} path="caption" className="text-small text-muted-foreground" />
        </figure>
      </Container>
    </Section>
  );
}
components/vibebox/hero-editorial/schema.ts
/**
 * VibeBox Motion · Editorial Hero
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
import { list, sections, text, z } from "@/lib/vibebox/content";

export const variants = ["framed", "type-led"] as const;

export type Variant = (typeof variants)[number];

export const content = sections.hero.extend({
  caption: text({ role: "Photo caption", max: 60, fillIn: true }).optional(),
  tags: list(text({ role: "Tag", max: 24 }), { role: "Tags", min: 0, max: 6 }),
});

export type Content = z.infer<typeof content>;

Content

Typed slots from schema.ts. Facts (hours, prices, quotes, names) come only from the business; until then they render as fill-ins.

SlotLabelTypeLimitFact
eyebrowKickertext32 chars—
headingHeadlinetext80 chars—
leadIntrotext160 chars—
primaryActionMain buttonaction24 chars—
secondaryActionSecond buttonaction24 chars—
mediaImageimage——
captionPhoto captiontext60 charsCan be a fill-in
tagsTagslistup to 6—
tags[]Tagtext24 chars—

Variants

  • Frameddefault

    framed

    Copy on the left, a tall 4:5 framed photo and caption on the right.

  • Type-led

    type-led

    The headline at hero size across the page, the photo as a slim band beneath.

Accessibility and motion

Keyboard
The actions, in reading order.
Screen readers
One h1, the intro, the actions, a list of tags, and a figure with its caption.
Touch
Nothing depends on hover.
Reduced motion
The photo appears without its reveal.

Edge cases and limits

  • No photo: a placeholder scene.
  • No caption: the figure has none.
  • Giant headline type about memorable brands with a ribbon scene in the corner

    Poster-size type that rises word by word on load, with the photo in a corner or set wide beneath.

    Heroes block · Server

  • A headline reading Beautiful websites for clinics, with the last word in spectrum colour

    A headline that names each audience in turn, the last word melting into the next.

    Heroes block · Server

  • A headline about moving better beside a mosaic of a tile scene, a bloom scene and a class list

    The promise beside a three-tile mosaic: a tall photo, a second image and a card of what the business offers.

    Heroes block · Server

↑ ↓ to move↵ to openesc to close