Menu

Change language

GitHub repo

Contact Us

blockmarketing

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

$npx shadcn@latest add https://uipkge.dev/r/lit/contact-us.json
Manifest:https://uipkge.dev/r/lit/contact-us.jsonInstalls to:src/uip/blocks/contact-us/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
access-tokenstring—optional
location[number, number][-122.4194, 37.7749]optional
location-labelstringSan Francisco, CAoptional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ContactUsSubmitPayload
interface ContactUsSubmitPayload {
  name: string
  email: string
  company: string
  subject: string
  message: string
}
HTMLElementTagNameMap
interface 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