Footer

Site footer with social media bar and multi-column link grid with alternating column widths

Installation

pnpm add @wandercom/design-system-web

Usage

The Footer component provides a comprehensive site footer with social media links, multi-column navigation, and branding. It supports both standard WanderOS and individual property sites through its variant system. Sites footers display a language selector when enabledLanguages contains at least two options. Use native language names for the option labels and use onLanguageChange to receive the selected language value.

27 lines
import { Footer } from '@wandercom/design-system-web/blocks/footer';
import { Logo } from '@wandercom/design-system-web/ui/logo';

export function Example() {
  return (
    <Footer
      topBar={{
        title: 'Follow us',
        links: [
          {
            label: 'X (Twitter)',
            href: 'https://x.com/wander',
            isExternal: true,
          },
        ],
      }}
      sections={[
        {
          title: 'Company',
          links: [{ label: 'About', href: '/about' }],
        },
      ]}
      logo={<Logo className="text-primary" />}
      logoSize="sm"
    />
  );
}

Example

The default footer variant includes trademark and copyright sections at the bottom and multi-column navigation. The Sites variant below documents the property-site footer with text social links, Company and Resources sections, and bottom legal links.

Loading example...
80 lines
<Footer
  topBar={{
    title: 'Follow us',
    links: [
      {
        label: 'X (Twitter)',
        href: 'https://x.com/wander',
        isExternal: true,
      },
      {
        label: 'Instagram',
        href: 'https://instagram.com/wander',
        isExternal: true,
      },
      {
        label: 'TikTok',
        href: 'https://tiktok.com/@wander',
        isExternal: true,
      },
      {
        label: 'LinkedIn',
        href: 'https://linkedin.com/company/wander',
        isExternal: true,
      },
      {
        label: 'YouTube',
        href: 'https://youtube.com/@wander',
        isExternal: true,
      },
    ],
  }}
  sections={[
    {
      title: 'Company',
      links: [
        { label: 'About', href: '/about' },
        { label: 'Blog', href: '/blog' },
        { 
          label: 'Contact', 
          key: 'contact',
          onClick: () =>
            toast.success({
              label: "Copied to clipboard",
              description: "Contact information copied",
            }),
        },
        { label: 'Careers', href: '/careers' },
        { label: 'Store', href: '/store' },
      ],
    },
    {
      title: 'Resources',
      links: [
        { label: 'Locations', href: '/locations' },
        { label: 'Licensing', href: '/licensing' },
        { label: 'Terms', href: '/terms' },
        { label: 'Privacy', href: '/privacy' },
        { label: 'Sitemap', href: '/sitemap' },
        { label: 'Events', href: '/events' },
      ],
    },
    {
      title: 'Hosts',
      links: [
        { label: 'WanderOS', href: '/sites' },
        { label: 'Operate with us', href: '/operate' },
        { label: 'List with us', href: '/listed' },
      ],
    },
    {
      title: 'Partners',
      links: [
        { label: 'Ambassadors', href: '/ambassadors' },
        { label: 'Travel Agents', href: '/travelagents' },
      ],
    },
  ]}
  logo={<Logo className="text-primary" />}
  logoSize="sm"
/>

A section can stack labelled clusters of links by providing groups instead of a flat links array. Each group takes an optional heading (a plain, non-interactive sub-heading exposed to assistive technology as text) plus its own links — for example one group per contact office when a site lists several. Spacing is applied per group, so a group without a heading is still separated from the others; the first group stays flush with the section title.

25 lines
<Footer
  variant="sites"
  sections={[
    {
      title: 'Contact us',
      groups: [
        { links: [{ label: 'Send us a message', href: '/message' }] },
        {
          heading: 'Myrtle Beach office',
          links: [
            { label: 'info@example.com', href: 'mailto:info@example.com' },
            { label: '603-770-9939', href: 'tel:6037709939' },
          ],
        },
        {
          // No heading — still separated by group spacing.
          links: [
            { label: 'hello@example.com', href: 'mailto:hello@example.com' },
            { label: '615-555-0142', href: 'tel:6155550142' },
          ],
        },
      ],
    },
  ]}
/>

Variants

The Footer component supports two variants optimized for different use cases.

Default variant

The default variant is designed for main Wander marketing and application sites. It features trademark and copyright sections at the bottom, and uses an alternating column width pattern for visual interest.

Sites variant

The Sites variant is optimized for individual property site footers. It replaces the trademark/copyright section with "Powered by WanderOS" branding, gives the logo half of the desktop grid, and divides the remaining half between two or three link sections. It can display contact actions in the top bar and bullet-separated legal links in the bottom row; on mobile those legal links render above the attribution and language selector.

Loading example...
94 lines
const copyContactValue = async (value: string) => {
  try {
    await navigator.clipboard.writeText(value);
    toast.success({ label: 'CopiedToClipboard', args: { value } });
  } catch {
    toast.error({ label: 'ErrorActionFailed' });
  }
};

const handleContactKeyPress = (
  event: React.KeyboardEvent<HTMLAnchorElement>,
  value: string,
) => {
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    copyContactValue(value);
  }
};

<Footer
  variant="sites"
  enabledLanguages={[
    { label: "English", value: "en" },
    { label: "Español", value: "es" },
    { label: "Français", value: "fr" },
    { label: "Deutsch", value: "de" },
    { label: "Português", value: "pt" },
  ]}
  logo={<WanderOSLogo className="text-primary" />}
  logoSize="sm"
  legalLinks={[
    { label: 'Terms of service', href: '/terms' },
    { label: 'Privacy policy', href: '/privacy' },
    { label: 'Sitemap', href: '/sitemap' },
  ]}
  onLanguageChange={(value) => setLanguage(value)}
  selectedLanguage={language}
  topBar={{
    title: 'Contact us',
    links: [
      {
        icon: <IconPhone className="size-4" />,
        key: 'support-phone',
        label: '(850) 260-3359',
        onClick: () => copyContactValue('(850) 260-3359'),
        onKeyPress: (event) => handleContactKeyPress(event, '(850) 260-3359'),
        showLabel: true,
      },
      {
        icon: <IconEmail className="size-4" />,
        key: 'support-email',
        label: 'info@downtimevacationrentals.com',
        onClick: () => copyContactValue('info@downtimevacationrentals.com'),
        onKeyPress: (event) => handleContactKeyPress(event, 'info@downtimevacationrentals.com'),
        showLabel: true,
      },
    ],
  }}
  sections={[
    {
      title: 'Company',
      links: [
        { label: 'About', href: '/about' },
        { label: 'List with us', href: '/list' },
        { label: 'Contact', href: '/contact' },
        { label: 'Blog', href: '/blog' },
      ],
    },
    {
      title: 'Social',
      links: [
        { label: 'X (Twitter)', href: 'https://x.com/wander', isExternal: true },
        { label: 'Instagram', href: 'https://instagram.com/wander', isExternal: true },
        { label: 'TikTok', href: 'https://tiktok.com/@wander', isExternal: true },
        { label: 'LinkedIn', href: 'https://linkedin.com/company/wander', isExternal: true },
        { label: 'YouTube', href: 'https://youtube.com/@wander', isExternal: true },
      ],
    },
    {
      title: 'Resources',
      links: [
        {
          label: 'Activities in Northern Kentucky',
          href: '/northern-kentucky',
        },
        { label: 'Activities in Nashville', href: '/nashville' },
        {
          label: 'Rental agreement',
          href: '/rental-agreement',
        },
      ],
    },
  ]}
/>

Language selector

The Sites variant displays the footer language selector next to the "Powered by WanderOS" attribution when enabledLanguages contains at least two options. Omit the prop, pass an empty array, or pass a single locale to hide the selector. Use each language's native name as its label, and use a stable locale code as its value.

12 lines
const [language, setLanguage] = useState("en");

<Footer
  variant="sites"
  enabledLanguages={[
    { label: "English", value: "en" },
    { label: "Español", value: "es" },
    { label: "Français", value: "fr" },
  ]}
  selectedLanguage={language}
  onLanguageChange={(value) => setLanguage(value)}
/>

onLanguageChange receives the selected locale value. The consuming site owns navigation: preserve the guest's current page when a localized version exists, and otherwise fall back to the localized homepage or an English equivalent.

The selector uses the design-system dropdown with keyboard navigation and a checkmark on the selected language. Its globe icon, current language, and chevron switch to the primary text color on hover and keyboard focus. The control is width-constrained for long native language names and wraps below the attribution when the footer becomes narrow. Override labels.languageSelectAriaLabel when the accessible label needs localization.

The Sites variant can display legalLinks in the bottom row alongside the attribution and language selector. Links are separated by bullets; on narrow screens the legal navigation appears before the attribution row. The consuming app owns which links appear and any route localization. Override labels.legalLinksAriaLabel when the legal navigation label needs localization.

The Footer component supports two types of links to accommodate different interaction patterns.

Most footer links use the href prop to navigate to pages. For external links, set isExternal: true to add proper security attributes. Top bar links render their label as visible text by default. When an icon is present, its label becomes accessible-only unless showLabel is true.

6 lines
{ label: 'Privacy', href: '/privacy' }
{
  label: 'Instagram',
  href: 'https://instagram.com/wander',
  isExternal: true,
}

For links that trigger actions instead of navigation (like copying to clipboard), use the onClick pattern. When using onClick, you must provide a unique key prop.

5 lines
{
  label: 'Contact',
  key: 'contact',
  onClick: () => toast.success({ label: 'Copied!' })
}

Layout

The footer uses a responsive Grid layout with different patterns for each variant:

Default variant:

  • Mobile (base): All sections span full width and stack vertically
  • Tablet (md): Sections alternate between 2 and 4 columns for visual rhythm (1st: 2 cols, 2nd: 4 cols, 3rd: 2 cols, 4th: 4 cols)
  • Desktop (lg): All sections span 2 columns within a 12-column grid

Sites variant:

  • Mobile (base): Logo and all link sections stack vertically
  • Tablet (md): Logo spans its own row, then two or three sections divide the six-column grid evenly
  • Desktop (lg): Logo spans 6 columns, then two or three sections divide the remaining 6 columns evenly

The logo section always spans full width on mobile. Use logoSize to apply a small, medium, or large height to SVG and image logo content while preserving its aspect ratio. The default is sm.

Props

variant?:

"default" | "sites"
Visual variant of the footer. The default variant displays trademark and copyright sections and uses alternating column widths. The sites variant displays "Powered by WanderOS" branding with a logo-half, two- or three-section desktop layout.

topBar?:

{ title?: string; links?: LinkItem[] }
Top bar configuration for links or actions. Icons replace visible labels by default; set showLabel on an item to display both.

sections?:

LinkSection[]
Array of navigation sections displayed as columns. Each section includes a title and array of links. Supports up to 4 sections in the default variant. Column spans vary by variant and breakpoint.

logo?:

React.ReactNode
Logo content displayed in the first column of the footer grid. Typically the Logo component or WanderOSLogo, but can be any React node including images or SVGs.

logoSize?:

"sm" | "md" | "lg"
Height applied to SVG and image logo content. Small uses 32 px, medium uses 48 px, and large uses 72 px. Defaults to "sm".

trademark?:

string
Trademark text displayed at the bottom of the footer (default variant only). Will have the registered trademark symbol appended.

enabledLanguages?:

FooterLanguage[]
Languages available in the sites variant selector. The selector is displayed only when at least two options are provided. Use native language names for labels and stable locale codes for values.

selectedLanguage?:

string
Currently selected locale value for the sites variant language selector.

onLanguageChange?:

(value: string) => void
Called with the selected locale value. The consumer is responsible for localized routing and fallback behavior.

labels?:

Partial<FooterLabels>
Overrides localizable footer strings, including languageSelectAriaLabel and legalLinksAriaLabel for sites variant accessible names.

className?:

string
Additional CSS classes to apply to the footer container.

FooterLanguage

label:

string
Native language name displayed in the selector, such as "Español" or "Français".

value:

string
Stable locale value passed to onLanguageChange when the option is selected.

LinkItem

label:

string
Display text for a navigation link, or the accessible name when an icon is used in the top bar.

href?:

string
URL the link navigates to. Use this for standard navigation links. Either href or onClick with key is required.

onClick?:

function
Function to call when the link is clicked. Use this for interactive links that trigger actions instead of navigation. When using onClick, you must provide a unique key prop.

onKeyPress?:

function
Function to call when a key is pressed while the link is focused. Only used with onClick links for keyboard accessibility.

key?:

string | number
Unique identifier for the link. Required when using onClick, optional when using href (defaults to href value).

isExternal?:

boolean
Indicates the link opens an external site. When true, automatically adds target="_blank" and rel="noopener noreferrer" for security. Use this for social media links and external resources.

icon?:

React.ReactNode
Consumer-provided icon. In topBar links, it replaces visible text while the label remains available to assistive technology.

showLabel?:

boolean
Displays a topBar link label alongside its icon. Defaults to false when an icon is present.
Footer