About Story
The business in its own words: a tall photo with a caption beside the story and a few honest details.
npx shadcn@latest add @vibebox/about-storyControls
<AboutStory />About
Rebuilds the site kit's about section. Free default for About in every template.
Use it when
- The business has a story worth a paragraph.
- There is a photo of the team or the place, or there will be.
Skip it when
- The story is one line: put it in the hero's intro.
Install
- 1Add the VibeBox registry to
components.json:{ "registries": { "@vibebox": "https://motion.vibeboxph.com/r/{name}.json" } } - 2Add the block with the shadcn CLI:
npx shadcn@latest add @vibebox/about-story - 3Use it:
import { AboutStory } from "@/components/vibebox/about-story"; import { home } from "@/content/pages/home"; <AboutStory content={home.about} variant="media-left" id="about" />
Code
2 filescomponents/vibebox/about-story/index.tsx
/**
* VibeBox Motion · About Story
* 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 { Container, headingId, Media, REVEAL, RichTextView, Section, SectionHead, 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";
/*
* Who the business is, in its own words: a tall photo with its caption beside the story and a
* few details (who it serves, what it is known for, its hours as a fill-in until given).
*/
const FALLBACK: Image = { src: "scene:horizon", alt: "", width: 1200, height: 1500 };
export type AboutStoryProps = BlockProps<Content, Variant>;
export function AboutStory({ content, variant = "media-left", id, tone, className }: AboutStoryProps) {
return (
<Section slug="about-story" variant={variant} id={id} tone={tone} labelledBy={headingId(id)} className={className}>
<Container className="grid grid-cols-[minmax(0,1fr)] items-center gap-[clamp(36px,5vw,80px)] md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<figure className={cn("m-0 grid gap-3", variant === "media-right" && "md:order-2", REVEAL)}>
<div className="relative aspect-[4/5] overflow-hidden rounded-xl ring-1 ring-border">
<Media image={content.media ?? FALLBACK} sizes="(min-width: 768px) 40vw, 100vw" />
<span aria-hidden="true" className="absolute right-4 bottom-4 size-12 rounded-lg bg-spectrum shadow-sm ring-4 ring-background/60" />
</div>
<SlotText as="figcaption" value={content.caption} path="caption" className="text-small text-muted-foreground" />
</figure>
<div className="grid content-center gap-7">
<SectionHead eyebrow={content.eyebrow} heading={content.heading} id={id} />
<RichTextView value={content.body} path="body" className={cn("max-w-[60ch] text-[1.075rem] leading-relaxed text-pretty text-muted-foreground", REVEAL)} />
{content.details.length > 0 && (
<dl className={cn("grid gap-x-8 gap-y-5 border-t border-border pt-6 sm:grid-cols-2", REVEAL)}>
{content.details.map((detail, index) => (
<div key={index} className="grid min-w-0 gap-1">
<SlotText as="dt" value={detail.label} path={`details.${index}.label`} className="text-small text-muted-foreground" />
<SlotText as="dd" value={detail.value} path={`details.${index}.value`} className="m-0 font-medium" />
</div>
))}
</dl>
)}
</div>
</Container>
</Section>
);
}components/vibebox/about-story/schema.ts
/**
* VibeBox Motion · About Story
* 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 { sections, text, type z } from "@/lib/vibebox/content";
export const variants = ["media-left", "media-right"] as const;
export type Variant = (typeof variants)[number];
export const content = sections.about.extend({
caption: text({ role: "Photo caption", max: 60, fillIn: true }).optional(),
});
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.
| Slot | Label | Type | Limit | Fact |
|---|---|---|---|---|
| eyebrow | Kicker | text | 32 chars | — |
| heading | Heading | text | 60 chars | — |
| body | Story | richText | 600 chars | — |
| media | Photo | image | — | — |
| details | Details | list | up to 4 | — |
| details[].label | Label | text | 24 chars | — |
| details[].value | Value | text | 60 chars | Can be a fill-in |
| caption | Photo caption | text | 60 chars | Can be a fill-in |
Variants
Photo leftdefault
media-left
Photo on the left, story on the right.
Photo right
media-right
Story on the left, photo on the right.
Accessibility and motion
- Keyboard
- Any links inside the story.
- Screen readers
- A heading, the story, a description list of details and the figure with its caption.
- Touch
- Nothing to touch.
- Reduced motion
- The photo and story appear without their reveal.
Edge cases and limits
- A 600-character story fits beside the photo at 1440 px; on phones it follows it.
- No photo: a placeholder scene.
Related

A tall photo stays in view while the story, details and milestones scroll past it, each revealing as it arrives.
About block · Server

Split Stage Hero
FreeThe promise beside a stage: your photo or a scene with two floating cards that show what booking or ordering looks like.
Heroes block · three.js

Bento Hero
FreeThe promise beside a three-tile mosaic: a tall photo, a second image and a card of what the business offers.
Heroes block · Server