Contact Us
blockmarketingTwo-column contact section. Left: eyebrow, headline, lede, icon contact rows (email / phone / office) and a live Mapbox map pinned to your office (pass access-token + location; falls back to a placeholder without a token). Right: a Card-wrapped form (name / email / company / subject Select / message Textarea) that swaps to a success state. Emits "submit" with the payload.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/contact-us.json$npx shadcn@latest add https://uipkge.dev/r/lit/contact-us.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/contact-us.json$bunx shadcn@latest add https://uipkge.dev/r/lit/contact-us.jsonManifest:
https://uipkge.dev/r/lit/contact-us.jsonInstalls to:src/uip/blocks/contact-us/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
access-token | string | — | optional |
location | [number, number] | [-122.4194, 37.7749] | optional |
location-label | string | San Francisco, CA | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ContactUsSubmitPayloadinterface ContactUsSubmitPayload {
name: string
email: string
company: string
subject: string
message: string
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-contact-us': UipContactUs
}
}Files installed (1)
src/uip/blocks/contact-us/contact-us.ts11.4 kB
import { LitElement, css, html } from 'lit' import { CheckCircle2, Mail, MapPin, Phone, Send } from 'lucide' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/button/button' import '../../components/input/input' import type { UipInput } from '../../components/input/input' import '../../components/label/label' import '../../components/select/select' import type { UipSelect } from '../../components/select/select' import '../../components/textarea/textarea' import type { UipTextarea } from '../../components/textarea/textarea' import '../../components/map/map' export interface ContactUsSubmitPayload { name: string email: string company: string subject: string message: string } /** * <uip-contact-us> — the registry ContactUs block as a web component. * * Class strings are React's ContactUs verbatim. React state (useState) * becomes @state fields; `onSubmit` (payload) becomes a `submit` CustomEvent * with the payload in `detail` (bubbles, composed). `accessToken` + * `location` drive a <uip-map> (mapbox-gl loads lazily inside it); without a * token the map area shows React's styled placeholder. Card chrome stays * plain divs carrying the registry Card part classes (see the login-01 * block). */ export class UipContactUs extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { accessToken: { attribute: 'access-token' }, location: { type: Array }, locationLabel: { attribute: 'location-label' }, name: { state: true }, email: { state: true }, company: { state: true }, subject: { state: true }, message: { state: true }, sent: { state: true }, } /** Mapbox token. Without it the map area shows a styled placeholder. */ accessToken = '' /** Office [lng, lat]. */ location: [number, number] = [-122.4194, 37.7749] locationLabel = 'San Francisco, CA' private name = '' private email = '' private company = '' private subject = 'sales' private message = '' private sent = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'contact-us') } private get canSubmit() { return !!this.name && !!this.email && !!this.message } private submit() { if (!this.canSubmit) return this.dispatchEvent( new CustomEvent<ContactUsSubmitPayload>('submit', { detail: { name: this.name, email: this.email, company: this.company, subject: this.subject, message: this.message }, bubbles: true, composed: true, }), ) this.sent = true } render() { return html` <section data-slot="contact-us" class="bg-background"> <div class="mx-auto max-w-6xl px-6 py-20"> <div class="grid gap-10 lg:grid-cols-2 lg:items-start"> <div class="space-y-6"> <p class="text-primary text-sm font-medium tracking-widest uppercase">Contact</p> <h2 class="text-3xl font-semibold tracking-tight sm:text-4xl">Talk to our team</h2> <p class="text-muted-foreground max-w-md"> Questions about pricing, onboarding, or a custom plan? Send a note and we'll get back within one business day. </p> <ul class="space-y-3"> <li class="flex items-center gap-3"> <span class="bg-muted text-foreground flex size-9 items-center justify-center rounded-lg"> ${icon(Mail, 'mail', 'size-4')} </span> <span class="text-sm">[email protected]</span> </li> <li class="flex items-center gap-3"> <span class="bg-muted text-foreground flex size-9 items-center justify-center rounded-lg"> ${icon(Phone, 'phone', 'size-4')} </span> <span class="text-sm">+1 (555) 010-2030</span> </li> <li class="flex items-center gap-3"> <span class="bg-muted text-foreground flex size-9 items-center justify-center rounded-lg"> ${icon(MapPin, 'map-pin', 'size-4')} </span> <span class="text-sm">${this.locationLabel}</span> </li> </ul> <div class="h-56 overflow-hidden rounded-xl border"> ${this.accessToken ? html`<uip-map access-token=${this.accessToken} .center=${this.location} zoom="11" muted class="size-full" > <uip-map-marker longitude=${this.location[0]} latitude=${this.location[1]} anchor="bottom"> <svg viewBox="0 0 24 24" width="26" height="34" class="text-primary drop-shadow"> <path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" fill="currentColor" stroke="#fff" stroke-width="1.5" /> <circle cx="12" cy="9" r="2.6" fill="#fff" /> </svg> </uip-map-marker> </uip-map>` : html`<div class="bg-muted text-muted-foreground flex size-full items-center justify-center gap-2 text-sm"> ${icon(MapPin, 'map-pin', 'size-4')} ${this.locationLabel} </div>`} </div> </div> <div data-uipkge="" data-slot="card" class="bg-card text-card-foreground rounded-xl border border-border shadow-sm transition-colors duration-150 overflow-hidden" > ${!this.sent ? html`<div data-uipkge="" data-slot="card-header" class="grid auto-rows-min grid-cols-[minmax(0,1fr)] grid-rows-[auto_auto] items-start gap-1.5 p-6 has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto]" > <h3 data-uipkge="" data-slot="card-title" class="leading-none font-semibold tracking-tight"> Send a message </h3> <p data-uipkge="" data-slot="card-description" class="text-muted-foreground text-sm"> We'll route it to the right person. </p> </div> <div data-uipkge="" data-slot="card-content" class="space-y-4 p-6 pt-0"> <div class="grid gap-4 sm:grid-cols-2"> <div class="space-y-1.5"> <uip-label for="cu-name">Name</uip-label> <uip-input id="cu-name" .value=${this.name} @input=${(e: Event) => { this.name = (e.target as UipInput).value }} placeholder="Jane Cooper" ></uip-input> </div> <div class="space-y-1.5"> <uip-label for="cu-email">Email</uip-label> <uip-input id="cu-email" .value=${this.email} @input=${(e: Event) => { this.email = (e.target as UipInput).value }} type="email" placeholder="[email protected]" ></uip-input> </div> </div> <div class="space-y-1.5"> <uip-label for="cu-company">Company</uip-label> <uip-input id="cu-company" .value=${this.company} @input=${(e: Event) => { this.company = (e.target as UipInput).value }} placeholder="Acme Inc." ></uip-input> </div> <div class="space-y-1.5"> <uip-label>Subject</uip-label> <uip-select placeholder="Choose a topic" .value=${this.subject} @change=${(e: Event) => { this.subject = (e.target as UipSelect).value }} > <option value="sales">Sales</option> <option value="support">Support</option> <option value="partnership">Partnership</option> </uip-select> </div> <div class="space-y-1.5"> <uip-label for="cu-message">Message</uip-label> <uip-textarea id="cu-message" .value=${this.message} @input=${(e: Event) => { this.message = (e.target as UipTextarea).value ?? '' }} rows="4" placeholder="How can we help?" ></uip-textarea> </div> <uip-button class="w-full" ?disabled=${!this.canSubmit} @click=${this.submit}> ${icon(Send, 'send', 'size-4')} Send message </uip-button> </div>` : html`<div data-uipkge="" data-slot="card-content" class="flex flex-col items-center gap-3 p-6 py-16 pt-0 text-center" > <span class="bg-success/10 text-success flex size-12 items-center justify-center rounded-full"> ${icon(CheckCircle2, 'circle-check-big', 'size-6')} </span> <p class="text-lg font-semibold">Message sent</p> <p class="text-muted-foreground max-w-xs text-sm"> Thanks, ${this.name} — we'll reply to ${this.email} within one business day. </p> <uip-button variant="outline" @click=${() => { this.sent = false }} > Send another </uip-button> </div>`} </div> </div> </div> </section> ` } } customElements.get('uip-contact-us') || customElements.define('uip-contact-us', UipContactUs) declare global { interface HTMLElementTagNameMap { 'uip-contact-us': UipContactUs } }
Raw manifest:https://uipkge.dev/r/lit/contact-us.json