Skip to content

Office locations

Addresses with opening hours and a live open-or-closed status.

Marketingstartercontactofficesaddresseshours

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import { Clock } from 'lucide-react'
import { Container } from '@/components/ui/layout'
import { Status } from '@/components/ui/status'

/**
 * Office locations
 *
 * Addresses as real `<address>` elements, with opening hours and a current
 * status. The status is the part people came for — knowing whether anyone is
 * there right now matters more than the postcode.
 */
const offices = [
  {
    city: 'London',
    lines: ['18 Ashfield Row', 'London EC2A 4NE', 'United Kingdom'],
    hours: 'Mon–Fri, 09:00–18:00 GMT',
    status: 'operational' as const,
    statusLabel: 'Open now',
    email: 'london@foundry.example',
  },
  {
    city: 'Lisbon',
    lines: ['Rua da Prata 42', '1100-414 Lisboa', 'Portugal'],
    hours: 'Mon–Fri, 09:00–18:00 WET',
    status: 'operational' as const,
    statusLabel: 'Open now',
    email: 'lisbon@foundry.example',
  },
  {
    city: 'Singapore',
    lines: ['9 Battery Road', 'Singapore 049910', 'Singapore'],
    hours: 'Mon–Fri, 09:00–18:00 SGT',
    status: 'idle' as const,
    statusLabel: 'Closed until 09:00',
    email: 'singapore@foundry.example',
  },
]

export default function OfficesContactSection() {
  return (
    <section className="border-b border-line bg-surface-sunken py-section">
      <Container>
        <div className="max-w-2xl">
          <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
            Three fictional offices.
          </h2>
        </div>

        <ul className="mt-10 grid gap-4 lg:grid-cols-3">
          {offices.map((office) => (
            <li key={office.city} className="rounded-xl border border-line bg-surface p-6">
              <div className="flex items-center justify-between gap-3">
                <h3 className="display-type text-lg font-semibold text-ink-strong">
                  {office.city}
                </h3>
                <Status appearance="pill" kind={office.status} label={office.statusLabel} />
              </div>
              <address className="mt-4 text-sm leading-relaxed text-ink-muted not-italic">
                {office.lines.map((line) => (
                  <span key={line} className="block">
                    {line}
                  </span>
                ))}
              </address>
              <p className="mt-4 flex items-center gap-2 text-xs text-ink-subtle">
                <Clock className="size-3.5" aria-hidden="true" />
                {office.hours}
              </p>
              <a
                href={`mailto:${office.email}`}
                className="mt-3 inline-block text-sm text-accent underline underline-offset-4"
              >
                {office.email}
              </a>
            </li>
          ))}
        </ul>
      </Container>
    </section>
  )
}

components/blocks/sections/contact/offices.tsx

import { Clock } from 'lucide-react'
import { Container } from '@/components/ui/layout'
import { Status } from '@/components/ui/status'

/**
 * Office locations
 *
 * Addresses as real `<address>` elements, with opening hours and a current
 * status. The status is the part people came for — knowing whether anyone is
 * there right now matters more than the postcode.
 */
const offices = [
  {
    city: 'London',
    lines: ['18 Ashfield Row', 'London EC2A 4NE', 'United Kingdom'],
    hours: 'Mon–Fri, 09:00–18:00 GMT',
    status: 'operational' as const,
    statusLabel: 'Open now',
    email: 'london@foundry.example',
  },
  {
    city: 'Lisbon',
    lines: ['Rua da Prata 42', '1100-414 Lisboa', 'Portugal'],
    hours: 'Mon–Fri, 09:00–18:00 WET',
    status: 'operational' as const,
    statusLabel: 'Open now',
    email: 'lisbon@foundry.example',
  },
  {
    city: 'Singapore',
    lines: ['9 Battery Road', 'Singapore 049910', 'Singapore'],
    hours: 'Mon–Fri, 09:00–18:00 SGT',
    status: 'idle' as const,
    statusLabel: 'Closed until 09:00',
    email: 'singapore@foundry.example',
  },
]

export default function OfficesContactSection() {
  return (
    <section className="border-b border-line bg-surface-sunken py-section">
      <Container>
        <div className="max-w-2xl">
          <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
            Three fictional offices.
          </h2>
        </div>

        <ul className="mt-10 grid gap-4 lg:grid-cols-3">
          {offices.map((office) => (
            <li key={office.city} className="rounded-xl border border-line bg-surface p-6">
              <div className="flex items-center justify-between gap-3">
                <h3 className="display-type text-lg font-semibold text-ink-strong">
                  {office.city}
                </h3>
                <Status appearance="pill" kind={office.status} label={office.statusLabel} />
              </div>
              <address className="mt-4 text-sm leading-relaxed text-ink-muted not-italic">
                {office.lines.map((line) => (
                  <span key={line} className="block">
                    {line}
                  </span>
                ))}
              </address>
              <p className="mt-4 flex items-center gap-2 text-xs text-ink-subtle">
                <Clock className="size-3.5" aria-hidden="true" />
                {office.hours}
              </p>
              <a
                href={`mailto:${office.email}`}
                className="mt-3 inline-block text-sm text-accent underline underline-offset-4"
              >
                {office.email}
              </a>
            </li>
          ))}
        </ul>
      </Container>
    </section>
  )
}

components/ui/status.tsx

import { CheckCircle2, AlertTriangle, XCircle, Info, CircleDashed, Clock } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Status
 *
 * Deliberately separate from Badge. A status describes the *state of a thing*
 * (a deployment, an invoice, a service) and therefore always pairs an icon
 * with a label — colour is the third signal, never the only one. That rule is
 * what makes the library legible to colour-blind users without a theme switch.
 */
export type StatusKind = 'operational' | 'degraded' | 'down' | 'pending' | 'info' | 'idle'

export interface StatusProps {
  kind: StatusKind
  label: string
  /** `dot` for dense tables, `pill` for standalone display. */
  appearance?: 'dot' | 'pill' | 'inline'
  className?: string
}

const config = {
  operational: {
    icon: CheckCircle2,
    text: 'text-success',
    bg: 'bg-success-soft border-success-line',
    dot: 'bg-success',
  },
  degraded: {
    icon: AlertTriangle,
    text: 'text-warning',
    bg: 'bg-warning-soft border-warning-line',
    dot: 'bg-warning',
  },
  down: {
    icon: XCircle,
    text: 'text-danger',
    bg: 'bg-danger-soft border-danger-line',
    dot: 'bg-danger',
  },
  pending: { icon: Clock, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
  info: { icon: Info, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
  idle: {
    icon: CircleDashed,
    text: 'text-ink-subtle',
    bg: 'bg-surface-sunken border-line',
    dot: 'bg-ink-subtle',
  },
} as const

export function Status({ kind, label, appearance = 'inline', className }: StatusProps) {
  const entry = config[kind]
  const Icon = entry.icon

  if (appearance === 'dot') {
    return (
      <span className={cn('inline-flex items-center gap-2 text-sm text-ink', className)}>
        <span className={cn('size-2 shrink-0 rounded-full', entry.dot)} aria-hidden="true" />
        {label}
      </span>
    )
  }

  if (appearance === 'pill') {
    return (
      <span
        className={cn(
          'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium',
          entry.bg,
          entry.text,
          className,
        )}
      >
        <Icon className="size-3.5 shrink-0" aria-hidden="true" />
        {label}
      </span>
    )
  }

  return (
    <span
      className={cn('inline-flex items-center gap-1.5 text-sm font-medium', entry.text, className)}
    >
      <Icon className="size-4 shrink-0" aria-hidden="true" />
      {label}
    </span>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

Knowing whether anyone is there right now matters more than the postcode, so the status is the most prominent element in each card.

  • Use real address elements; they carry meaning and need only the italic removed.
  • State the timezone with the hours, or the hours are useless to a remote reader.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Three offices
  • Open status
  • Real address elements

Accessibility

Address semantics
Each office is an address element with a heading.
Status pills
Open and closed are carried by icon and word, not colour.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.