Skip to content
Motion
FreeIn reviewv1.0.0

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-split

Controls

Variant
<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

  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/contact-split
  3. 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 files
components/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.

SlotLabelTypeLimitFact
eyebrowKickertext32 chars—
headingHeadingtext60 chars—
leadIntrotext160 chars—
channelsWays to reach youlistup to 6—
channels[].kindChannelchoice——
channels[].labelLabeltext24 chars—
channels[].valueValuefact120 charsYes, a fill-in until given
form.nameLabelName fieldtext24 chars—
form.emailLabelEmail fieldtext24 chars—
form.messageLabelMessage fieldtext40 chars—
form.submitLabelSend buttontext24 chars—
form.noteForm notetext120 chars—
mediaMap or photoimage——

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 link channel links only to https addresses.
  • Four contact cards with icons: visit, hours, call and email

    One card per way to reach the business (call, visit, message), or a list of rows; facts until given.

    Contact block · Server

  • A compact card with a heading, four contact channels and a Book a visit button

    A compact contact band: the heading, every channel on one line and one action.

    Contact block · Server

  • Contact channels, a Message us bloom button and an inquiry form

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

    Contact block · Server

↑ ↓ to move↵ to openesc to close