Skip to content
Motion
FreeIn reviewv1.0.0

Split Bloom Contact

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

npx shadcn@latest add @vibebox/contact-split-bloom

Controls

Variant
<ContactSplitBloom />

About

Built on the library's Contact Bloom for businesses whose customers prefer chat apps (common in the Philippines). It differs from contact-split by the bloom.

Use it when

  • Customers message more than they email.

Skip it when

  • The business has no phone or chat channels: use contact-split.

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-bloom
  3. 3Use it:
    import { ContactSplitBloom } from "@/components/vibebox/contact-split-bloom";
    import { home } from "@/content/pages/home";
    
    <ContactSplitBloom content={home.contact} variant="form-right" id="contact" />

Code

2 files
components/vibebox/contact-split-bloom/index.tsx
/**
 * VibeBox Motion · Split Bloom 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 { ContactBloom, type ContactChannels } from "@/components/vibebox/contact-bloom";
import { isFillIn } from "@/lib/vibebox/content/runtime";
import { cn } from "@/lib/utils";
import type { Content, Variant } from "./schema";

/*
 * The split contact section with a quick way out: beside the channels, a Contact Bloom button
 * opens the phone, text and chat apps the business uses. It shows once at least one of those
 * channels is a real value, never for fill-ins.
 */

const BLOOM_KINDS = ["phone", "sms", "viber", "messenger", "whatsapp", "email"] as const;

function bloomChannels(channels: Content["channels"]): ContactChannels {
  const found: ContactChannels = {};
  for (const channel of channels) {
    const kind = BLOOM_KINDS.find((candidate) => candidate === channel.kind);
    if (kind && !isFillIn(channel.value) && !found[kind]) found[kind] = channel.value;
  }
  return found;
}

export type ContactSplitBloomProps = BlockProps<Content, Variant> & {
  /** Where the form posts (assembled apps handle /api/inquiries). */
  formAction?: string;
};

export function ContactSplitBloom({ content, variant = "form-right", id, tone, preview, formAction, className }: ContactSplitBloomProps) {
  const quick = bloomChannels(content.channels);
  return (
    <Section slug="contact-split-bloom" 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} />
          {content.media && (
            <div className={cn("aspect-[21/9] overflow-hidden rounded-xl ring-1 ring-border", REVEAL)}>
              <Media image={content.media} sizes="(min-width: 1024px) 45vw, 100vw" />
            </div>
          )}
          <ChannelList channels={content.channels} columns className={REVEAL} />
          <div data-slot="bloomLabel" className={cn("justify-self-start", REVEAL)}>
            {Object.keys(quick).length > 0 ? (
              <ContactBloom channels={quick} label={content.bloomLabel} direction="down" />
            ) : (
              <span className="inline-flex h-11 items-center rounded-full border border-dashed border-border-strong px-5 text-small text-muted-foreground">{content.bloomLabel}</span>
            )}
          </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-bloom/schema.ts
/**
 * VibeBox Motion · Split Bloom 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, text, 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.extend({
  bloomLabel: text({ role: "Quick contact button", max: 24 }),
});

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——
bloomLabelQuick contact buttontext24 chars—

Variants

  • Form rightdefault

    form-right

    Channels and bloom left, the form right.

  • Form left

    form-left

    The form left, channels and bloom right on desktop.

Accessibility and motion

Keyboard
Channel links, the bloom button (Enter opens, Escape closes), then the form.
Screen readers
The bloom is a disclosure button with a list of channel links; the form has visible labels.
Touch
The bloom opens by tap; its items are 44 px tall.
Reduced motion
The bloom opens without its spring.

Edge cases and limits

  • Only fill-ins for phone and chat: the bloom shows as a dashed placeholder.
  • The bloom opens downward so it never covers the heading.
  • A chat panel with a greeting and three quick questions beside contact channels

    Contact as a conversation: a chat panel whose quick questions open the business's chat app, prefilled.

    Contact block · Server

  • Contact channels with icons and a map scene beside an inquiry form

    Every way to reach the business beside an inquiry form that works without JavaScript; facts are fill-ins until given.

    Contact block · Server

  • 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

↑ ↓ to move↵ to openesc to close