Contact Strip
A compact contact band: the heading, every channel on one line and one action.
npx shadcn@latest add @vibebox/contact-stripControls
<ContactStrip />About
Rebuilds the site kit's compact contact strip, used on FAQ and inner pages.
Use it when
- An inner page ends with a reminder of how to get in touch.
Skip it when
- This is the site's main contact section: use
contact-split.
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-strip - 3Use it:
import { ContactStrip } from "@/components/vibebox/contact-strip"; import { home } from "@/content/pages/home"; <ContactStrip content={home.contact} variant="row" id="contact" />
Code
2 filescomponents/vibebox/contact-strip/index.tsx
/**
* VibeBox Motion · Contact Strip
* 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 { ActionLink, ChannelValue, CHANNEL_ICONS, Container, Eyebrow, headingId, Icon, REVEAL, Section, SlotText, type BlockProps } from "@/lib/vibebox/block-kit";
import { cn } from "@/lib/utils";
import type { Content, Variant } from "./schema";
/*
* A compact contact band: the heading, every channel on one line, and one action. For the end of
* an inner page, or above the footer when the full contact section lives elsewhere.
*/
export type ContactStripProps = BlockProps<Content, Variant>;
export function ContactStrip({ content, variant = "row", id, tone, className }: ContactStripProps) {
const centered = variant === "centered";
return (
<Section slug="contact-strip" variant={variant} id={id} tone={tone} labelledBy={headingId(id)} spacing={false} className={cn("py-[clamp(40px,6vw,80px)]", className)}>
<Container>
<div
className={cn(
"grid gap-8 rounded-xl bg-card p-6 text-card-foreground ring-1 ring-border sm:p-8",
centered ? "justify-items-center text-center" : "lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.6fr)_auto] lg:items-center lg:gap-12",
REVEAL,
)}
>
<div className={cn("grid gap-3", centered && "justify-items-center")}>
<Eyebrow value={content.eyebrow} />
<SlotText as="h2" value={content.heading} path="heading" id={headingId(id)} className="font-display text-[clamp(1.6rem,1.2rem+1.4vw,2.4rem)] leading-tight font-semibold tracking-[-0.025em] text-balance" />
<SlotText as="p" value={content.lead} path="lead" className="max-w-[48ch] text-small text-pretty text-muted-foreground" />
</div>
<dl className={cn("grid gap-x-8 gap-y-4 sm:grid-cols-2", centered ? "w-full max-w-4xl text-left lg:grid-cols-[repeat(auto-fit,minmax(12rem,1fr))]" : "")}>
{content.channels.map((channel, index) => (
<div key={index} className="relative grid min-w-0 gap-0.5 pl-6.5">
<dt data-slot={`channels.${index}.label`} className="text-caption text-muted-foreground">
<Icon name={CHANNEL_ICONS[channel.kind]} className="absolute top-0.5 left-0 size-4 text-muted-foreground" />
{channel.label}
</dt>
<dd className="m-0 min-w-0 text-small font-medium">
<ChannelValue channel={channel} path={`channels.${index}.value`} />
</dd>
</div>
))}
</dl>
{content.action && <ActionLink action={content.action} path="action" className="justify-self-start lg:justify-self-end" />}
</div>
</Container>
</Section>
);
}components/vibebox/contact-strip/schema.ts
/**
* VibeBox Motion · Contact Strip
* 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 { action, sections, type z } from "@/lib/vibebox/content";
export const variants = ["row", "centered"] as const;
export type Variant = (typeof variants)[number];
export const content = sections.contact.omit({ form: true, media: true }).extend({
action: action({ role: "Button" }).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 | — |
| 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 |
| action | Button | action | 24 chars | — |
Variants
Rowdefault
row
Heading, channels and action in one row on desktop.
Centred
centered
Centred heading, channels in a wrapping grid, action beneath.
Accessibility and motion
- Keyboard
- Channel links, then the action.
- Screen readers
- A heading and a description list of channels.
- Touch
- Links are full-width rows on phones.
- Reduced motion
- The strip appears without its reveal.
Edge cases and limits
- No action: the strip is heading and channels.
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

Split Contact
FreeEvery way to reach the business beside an inquiry form that works without JavaScript; facts are fill-ins until given.
Contact block · Server

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