Split Contact
Every way to reach the business beside an inquiry form that works without JavaScript; facts are fill-ins until given.
npx shadcn@latest add @vibebox/contact-splitControls
<ContactSplit />About
Rebuilds the site kit's classic split and split form contact layouts. The free default contact section in every template. Every phone, address, email and hour is a fact the client gives.
Use it when
- Customers reach the business by message, call or visit.
Skip it when
- The page only needs a short band of channels: use
contact-strip.
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/contact-split - 3Use it:
import { ContactSplit } from "@/components/vibebox/contact-split"; import { home } from "@/content/pages/home"; <ContactSplit content={home.contact} variant="form-right" id="contact" />
Code
2 filescomponents/vibebox/contact-split/index.tsx
/**
* VibeBox Motion · Split Contact
* 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 { ChannelList, Container, headingId, InquiryForm, Media, REVEAL, Section, SectionHead, type BlockProps } from "@/lib/vibebox/block-kit";
import { cn } from "@/lib/utils";
import type { Content, Variant } from "./schema";
/*
* Every way to reach the business beside an inquiry form: the channels (facts, fill-ins until
* given), a map or photo, and a plain HTML form that posts to the app's inquiry route.
*/
export type ContactSplitProps = BlockProps<Content, Variant> & {
/** Where the form posts (assembled apps handle /api/inquiries). */
formAction?: string;
};
export function ContactSplit({ content, variant = "form-right", id, tone, preview, formAction, className }: ContactSplitProps) {
return (
<Section slug="contact-split" variant={variant} id={id} tone={tone} labelledBy={headingId(id)} className={className}>
<Container className="grid grid-cols-[minmax(0,1fr)] gap-[clamp(36px,5vw,72px)] lg:grid-cols-2">
<div className={cn("grid content-start gap-8", variant === "form-left" && "lg:order-2")}>
<SectionHead eyebrow={content.eyebrow} heading={content.heading} lead={content.lead} id={id} />
<ChannelList channels={content.channels} columns className={REVEAL} />
{content.media && (
<div className={cn("aspect-[16/9] overflow-hidden rounded-xl ring-1 ring-border", REVEAL)}>
<Media image={content.media} sizes="(min-width: 1024px) 45vw, 100vw" />
</div>
)}
</div>
{content.form && (
<div className={cn("self-start rounded-xl bg-card p-6 text-card-foreground ring-1 ring-border sm:p-8", REVEAL)}>
<InquiryForm form={content.form} action={formAction} preview={preview} source={id} />
</div>
)}
</Container>
</Section>
);
}components/vibebox/contact-split/schema.ts
/**
* VibeBox Motion · Split Contact
* 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, type z } from "@/lib/vibebox/content";
export const variants = ["form-right", "form-left"] as const;
export type Variant = (typeof variants)[number];
export const content = sections.contact;
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 | — |
| lead | Intro | text | 160 chars | — |
| channels | Ways to reach you | list | up to 6 | — |
| channels[].kind | Channel | choice | — | — |
| channels[].label | Label | text | 24 chars | — |
| channels[].value | Value | fact | 120 chars | Yes, a fill-in until given |
| form.nameLabel | Name field | text | 24 chars | — |
| form.emailLabel | Email field | text | 24 chars | — |
| form.messageLabel | Message field | text | 40 chars | — |
| form.submitLabel | Send button | text | 24 chars | — |
| form.note | Form note | text | 120 chars | — |
| media | Map or photo | image | — | — |
Variants
Form rightdefault
form-right
Channels and map left, the form on a card right.
Form left
form-left
The form left, channels and map right on desktop.
Accessibility and motion
- Keyboard
- Channel links, then the form's fields and its button.
- Screen readers
- Channels are a description list; every field has a visible label; required fields are marked by the browser.
- Touch
- Fields are 44 px tall; channel links are full rows.
- Reduced motion
- Panels appear without their reveal.
Edge cases and limits
- Six long channels wrap within their column.
- No form in content: the channels stand alone.
- An address links to a map search; a
linkchannel links only to https addresses.
Related

Contact Cards
FreeOne card per way to reach the business (call, visit, message), or a list of rows; facts until given.
Contact block · Server

Contact Strip
FreeA compact contact band: the heading, every channel on one line and one action.
Contact block · Server

The split contact section with a Contact Bloom button that opens the phone and chat apps the business uses.
Contact block · Server