caliper uidocs

Contact template

Block

A contact page that routes the message first: pick the topic and it says who answers and how soon. The form keeps what you typed if sending fails and confirms where the reply will go.

Installs components/ui/contact-template.tsxDepends on @radix-ui/react-popover, @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/contact-template.json
Skip to content

Contact

Talk to a person

Choose what it is about and we will say who answers and how soon. Type “fail” in the message to see a failed send.

  • Pricing and plansThe sales team replies within one working day.sales@halden.app
  • Help with my accountSupport replies within four working hours, 8:00–18:00 Lisbon time.help@halden.app
  • Security or complianceOur security lead replies within two working days. For an urgent issue, write to security@halden.app.security@halden.app
  • PressJoana Reis replies within two working days.press@halden.app

Send a message

What is it about?

The sales team replies within one working day.

0 / 2000

We only use your details to reply.

Offices

FAQ

Before you write

Straight answers, each linked to the page that explains it in full.

Not here? Ask us about it

  • No. It prepares the close; your accountant reviews it and signs off, usually faster. How review works

When to use #

Use it for

  • Sales, support and press enquiries on one page.
  • Teams with offices people visit.

Skip it when

  • Signed-in customers with an account; give them in-app support.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/contact-template.json

Adds its 16 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { ContactTemplate } from "@/components/ui/contact-template"

const nav = (current: string) =>
  [
    { label: "Product", href: "#product" },
    { label: "Customers", href: "#customers" },
    { label: "Blog", href: "#blog" },
    { label: "About", href: "#about" },
    { label: "Contact", href: "#contact" },
  ].map((link) => ({ ...link, current: link.label.toLowerCase() === current }));

const signIn = { label: "Sign in", href: "#" };

const frame = "h-[720px] w-full overflow-auto rounded-[14px] shadow-[0_0_0_1px_var(--border)]";

const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

export function ContactDemo() {
  return (
    <div className={frame}>
      <ContactTemplate
        brand="Halden"
        nav={nav("contact")}
        signIn={signIn}
        primary={heroes.primary!}
        intro={{ eyebrow: "Contact", title: "Talk to a person", lead: "Choose what it is about and we will say who answers and how soon. Type “fail” in the message to see a failed send." }}
        topics={[
          { value: "sales", label: "Pricing and plans", response: "The sales team replies within one working day.", email: "sales@halden.app" },
          { value: "support", label: "Help with my account", response: "Support replies within four working hours, 8:00–18:00 Lisbon time.", email: "help@halden.app" },
          { value: "security", label: "Security or compliance", response: "Our security lead replies within two working days. For an urgent issue, write to security@halden.app.", email: "security@halden.app" },
          { value: "press", label: "Press", response: "Joana Reis replies within two working days.", email: "press@halden.app" },
        ]}
        offices={[
          { city: "Lisbon", address: "Rua do Ouro 120, 3º, 1100-063 Lisboa", hours: "Mon–Fri 9:00–18:00", timezone: "WET", mapHref: "#lisbon" },
          { city: "London", address: "71 Leonard Street, London EC2A 4QS", hours: "Mon–Thu 9:00–18:00", timezone: "GMT", mapHref: "#london" },
        ]}
        faq={{ ...faqs, title: "Before you write", items: faqs.items.slice(0, 4) }}
        onSubmit={async (message) => {
          await wait(800);
          if (message.message.toLowerCase().includes("fail")) throw new Error("Our mail server did not answer.");
        }}
        footer={footers}
      />
    </div>
  );
}

API reference #

ContactTemplate

component
PropTypeDefaultDescription
brand *string—
navreadonly BlockNavLink[]—
signInBlockAction—
primaryBlockAction—
intro *{ eyebrow?: string | undefined; title: string; lead?: string | undefined; }—
topics *readonly ContactTopic[]—
officesreadonly ContactOffice[][]
faqWithout<{ eyebrow?: string | undefined; title: string; lead?: string | undefined; items: readonly BlockFaq[]; searchable?: boolean | undefined; ask?: { label…—
onSubmit *(message: ContactMessage) => void | Promise<void>—Send the message. A thrown error is shown above the form and nothing typed is lost.
footerTemplateFooter—
classNamestring—

Accessibility #

It exposes status semantics, with aria-label, aria-hidden, aria-labelledby and aria-live kept in step with state. Changes that matter are announced through a live region.

Keyboard and focus handling comes from Radix popover: arrow keys move through items, Enter and Space select, and Escape closes and returns focus.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.