# Footer 8

> Photo footer with newsletter, link columns, offices and socials (from Marbrook)

A shadcn/ui block from the [@cnippet-blocks](https://blocks.cnippet.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cnippet-blocks/footer-8).

- Page, with a live preview: https://shadcn.exchange/r/cnippet-blocks/footer-8
- Screenshot: https://shadcn.exchange/shot/cnippet-blocks/footer-8/light.png
- Registry item: https://blocks.cnippet.dev/r/footer-8.json
- Categories: footer
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/cnippet-blocks/footer-8.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `@cnippet/button`, `@cnippet/input`, `@cnippet/tooltip`, `@cnippet/utils`

## Files

### `registry/default/sections/footer-8/index.tsx`

```tsx
import { ArrowRight, Mail } from "lucide-react";
import Image from "next/image";
import Link from "next/link";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";

import type { ImageAsset } from "../_types/common";
import type { FooterContent } from "../_types/footer";
import { defaultFooter8Background, defaultFooter8Content } from "./data";
import { socialIcons } from "./social-icons";

function telHref(phone: string): string {
  return `tel:${phone.replace(/[^\d+]/g, "")}`;
}

/**
 * Over a photograph the copy stays literal white: `--primary-foreground`
 * inverts in dark mode and would vanish against the picture.
 */
const tones = {
  photo: {
    border: "border-white/10",
    faint: "text-white/40",
    muted: "text-white/60",
    soft: "text-white/70",
    text: "text-white",
  },
  primary: {
    border: "border-primary-foreground/10",
    faint: "text-primary-foreground/40",
    muted: "text-primary-foreground/60",
    soft: "text-primary-foreground/70",
    text: "text-primary-foreground",
  },
};

export default function Footer8({
  backgroundImage = defaultFooter8Background,
  content = defaultFooter8Content,
  onPhoto = false,
}: {
  /** Photo behind the footer. Pass `null` for a plain primary band. */
  backgroundImage?: ImageAsset | null;
  content?: FooterContent;
  /** A parent already paints a photo behind this footer (and usually the CTA
   *  above it): stay transparent but keep the photo text colours. Use with
   *  `backgroundImage={null}`. */
  onPhoto?: boolean;
}) {
  const photo = Boolean(backgroundImage) || onPhoto;
  const t = photo ? tones.photo : tones.primary;
  const year = new Date().getFullYear();

  return (
    <footer
      className={cn(
        "relative isolate overflow-hidden",
        t.text,
        !photo && "bg-primary",
      )}
    >
      {backgroundImage && (
        <>
          <Image
            alt={backgroundImage.alt}
            className="-z-10 object-cover"
            fill
            sizes="100vw"
            src={backgroundImage.src}
          />
          <div className="absolute inset-0 -z-10 bg-primary/55" />
        </>
      )}
      <div className="absolute inset-0 -z-10 bg-linear-to-t from-primary" />

      <div className="mx-auto max-w-7xl px-5 sm:px-8 lg:px-12">
        <div
          className={cn(
            "grid gap-12 border-b py-16 lg:grid-cols-12 lg:gap-8 lg:py-20",
            t.border,
          )}
        >
          <div className="lg:col-span-5">
            <span className="font-display text-3xl leading-none">
              {content.brand.name}
            </span>
            <p className={cn("mt-5 max-w-sm text-sm leading-relaxed", t.muted)}>
              {content.brand.description}
            </p>

            {content.newsletter && (
              <div className="mt-8">
                <p
                  className={cn(
                    "inline-flex items-center gap-2 font-medium text-[0.6875rem] uppercase tracking-[0.22em]",
                    t.faint,
                  )}
                >
                  <span className="size-1 bg-secondary" />
                  {content.newsletter.eyebrow}
                </p>
                <form className="mt-4 flex max-w-sm items-center gap-0">
                  <Input
                    aria-label="Email address"
                    className={cn(
                      "h-11 items-center rounded-r-none border-current/20 bg-current/5 placeholder:text-current/40 focus-visible:border-secondary",
                      t.text,
                    )}
                    name="email"
                    placeholder={content.newsletter.placeholder}
                    type="email"
                  />
                  <Tooltip>
                    <TooltipTrigger
                      render={
                        <Button
                          aria-label={content.newsletter.buttonLabel}
                          className="h-11 shrink-0 rounded-l-none border-none bg-secondary px-4 text-secondary-foreground shadow-none hover:bg-secondary/90"
                          type="submit"
                        >
                          <ArrowRight className="size-4" />
                        </Button>
                      }
                    />
                    <TooltipPopup>
                      {content.newsletter.buttonLabel}
                    </TooltipPopup>
                  </Tooltip>
                </form>
                {content.newsletter.note && (
                  <p className={cn("mt-3 text-xs", t.faint)}>
                    {content.newsletter.note}
                  </p>
                )}
              </div>
            )}
          </div>

          <div className="grid gap-10 sm:grid-cols-3 lg:col-span-7">
            {content.columns.map((column) => (
              <div key={column.title}>
                <h3
                  className={cn(
                    "inline-flex items-center gap-2 font-medium text-[0.6875rem] uppercase tracking-[0.22em]",
                    t.faint,
                  )}
                >
                  {column.title}
                </h3>
                <ul className="mt-5 space-y-3">
                  {column.links.map((link) => (
                    <li key={link.label}>
                      <Link
                        className={cn(
                          "text-sm transition-colors hover:text-secondary",
                          t.soft,
                        )}
                        href={link.href}
                      >
                        {link.label}
                      </Link>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        </div>

        {(content.contact || content.socials) && (
          <div
            className={cn(
              "grid gap-10 border-b py-10 sm:grid-cols-2 lg:grid-cols-4",
              t.border,
            )}
          >
            {content.contact?.locations.map((location) => (
              <div key={location.label}>
                <h4 className="font-medium text-sm">{location.label}</h4>
                <p className={cn("mt-2 text-sm leading-relaxed", t.muted)}>
                  {location.address}
                </p>
                {location.phone && (
                  <a
                    className={cn(
                      "mt-1 inline-block text-sm transition-colors hover:text-secondary",
                      t.muted,
                    )}
                    href={telHref(location.phone)}
                  >
                    {location.phone}
                  </a>
                )}
              </div>
            ))}

            {content.contact?.email && (
              <div>
                <h4 className="font-medium text-sm">
                  {content.contact.email.label}
                </h4>
                <a
                  className={cn(
                    "mt-2 inline-flex items-center gap-2 text-sm transition-colors hover:text-secondary",
                    t.muted,
                  )}
                  href={`mailto:${content.contact.email.address}`}
                >
                  <Mail className="size-4" />
                  {content.contact.email.address}
                </a>
              </div>
            )}

            {content.socials && content.socials.length > 0 && (
              <div>
                {content.socialsTitle && (
                  <h4 className="font-medium text-sm">
                    {content.socialsTitle}
                  </h4>
                )}
                <div className="mt-3 flex gap-2">
                  {content.socials.map((social) => {
                    const Icon = socialIcons[social.network];

                    return (
                      <Tooltip key={social.label}>
                        <TooltipTrigger
                          render={
                            <Link
                              aria-label={social.label}
                              className={cn(
                                "flex size-9 items-center justify-center rounded-md border border-current/15 transition-colors hover:border-secondary hover:text-secondary",
                                t.soft,
                              )}
                              href={social.href}
                            >
                              <Icon className="size-4" />
                            </Link>
                          }
                        />
                        <TooltipPopup>{social.label}</TooltipPopup>
                      </Tooltip>
                    );
                  })}
                </div>
              </div>
            )}
          </div>
        )}

        <div
          className={cn(
            "flex flex-col items-center justify-between gap-4 py-8 text-xs sm:flex-row",
            t.muted,
          )}
        >
          <p>
            © {year} {content.copyright}
          </p>
          <div className="flex flex-wrap items-center justify-center gap-6">
            {content.legal.map((link) => (
              <Link
                className="transition-colors hover:text-secondary"
                href={link.href}
                key={link.href}
              >
                {link.label}
              </Link>
            ))}
            {content.credit && (
              <span>
                {content.credit.prefix}{" "}
                <Link
                  className="underline underline-offset-2"
                  href={content.credit.href}
                >
                  {content.credit.label}
                </Link>
              </span>
            )}
          </div>
        </div>
      </div>
    </footer>
  );
}
```

### `registry/default/sections/footer-8/data.ts`

```ts
import type { ImageAsset } from "../_types/common";
import type { FooterContent } from "../_types/footer";

export const defaultFooter8Content: FooterContent = {
  brand: {
    description:
      "A property practice built on discretion, local knowledge, and thirty years of transactions across the harbour and the hills.",
    name: "Marbrook",
  },
  columns: [
    {
      links: [
        {
          href: "/properties",
          label: "All listings",
        },
        {
          href: "/properties?status=sale",
          label: "For sale",
        },
        {
          href: "/properties?status=rent",
          label: "For rent",
        },
        {
          href: "/properties?status=new",
          label: "New developments",
        },
        {
          href: "/services#selling",
          label: "Off-market selling",
        },
      ],
      title: "Properties",
    },
    {
      links: [
        {
          href: "/services#buying",
          label: "Buyer advisory",
        },
        {
          href: "/services#selling",
          label: "Seller representation",
        },
        {
          href: "/services#valuation",
          label: "Free valuation",
        },
        {
          href: "/services#management",
          label: "Property management",
        },
        {
          href: "/services#relocation",
          label: "Relocation",
        },
      ],
      title: "Services",
    },
    {
      links: [
        {
          href: "/about",
          label: "Our story",
        },
        {
          href: "/team",
          label: "Advisors",
        },
        {
          href: "/careers",
          label: "Careers",
        },
        {
          href: "/journal",
          label: "Journal",
        },
        {
          href: "/contact",
          label: "Contact",
        },
      ],
      title: "Company",
    },
  ],
  contact: {
    email: {
      address: "hello@marbrook.com",
      label: "Enquiries",
    },
    locations: [
      {
        address: "148 Rowan Street, Harbour District",
        label: "Harbour District",
        phone: "+1 (800) 555-0199",
      },
      {
        address: "9 Vantage Lane, Northgate Heights",
        label: "Northgate Heights",
        phone: "+1 (800) 555-0142",
      },
    ],
  },
  copyright: "Marbrook Estates. All rights reserved.",
  credit: {
    href: "https://cnippet.com",
    label: "Cnippet",
    prefix: "Designed by",
  },
  legal: [
    {
      href: "/legal#privacy",
      label: "Privacy",
    },
    {
      href: "/legal#terms",
      label: "Terms",
    },
    {
      href: "/legal#fair-housing",
      label: "Fair housing",
    },
  ],
  newsletter: {
    buttonLabel: "Subscribe",
    eyebrow: "Market letter",
    note: "Quarterly pricing data. No spam, unsubscribe anytime.",
    placeholder: "you@email.com",
  },
  socials: [
    {
      href: "#",
      label: "Instagram",
      network: "instagram",
    },
    {
      href: "#",
      label: "LinkedIn",
      network: "linkedin",
    },
    {
      href: "#",
      label: "Facebook",
      network: "facebook",
    },
  ],
  socialsTitle: "Follow",
};

export const defaultFooter8Background: ImageAsset = {
  alt: "Evening light across a harbour residence",
  src: "https://res.cloudinary.com/qgiongel/image/upload/q_auto/f_auto/v1786950553/templates/estate/marbrook/cndn_marbrook003.jpg",
};
```

### `registry/default/sections/footer-8/social-icons.tsx`

```tsx
import type { SVGProps } from "react";

import type { SocialNetwork } from "../_types/footer";

// lucide-react v1 removed brand marks, so the social glyphs live here.
type IconProps = SVGProps<SVGSVGElement>;

export const socialIcons: Record<
  SocialNetwork,
  (props: IconProps) => React.ReactElement
> = {
  facebook: (props) => (
    <svg
      aria-hidden="true"
      fill="currentColor"
      height="24"
      viewBox="0 0 24 24"
      width="24"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path d="M14 13.5h2.5l1-4H14v-2c0-1.03 0-2 2-2h1.5V2.14c-.326-.043-1.557-.14-2.857-.14C11.928 2 10 3.657 10 6.7v2.8H7v4h3V22h4v-8.5Z" />
    </svg>
  ),
  instagram: (props) => (
    <svg
      aria-hidden="true"
      fill="currentColor"
      height="24"
      viewBox="0 0 24 24"
      width="24"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path d="M12 2c2.717 0 3.056.01 4.122.06 1.065.05 1.79.217 2.428.465.66.254 1.216.598 1.772 1.153a4.908 4.908 0 0 1 1.153 1.772c.247.637.415 1.363.465 2.428.048 1.066.06 1.405.06 4.122 0 2.717-.01 3.056-.06 4.122-.05 1.065-.218 1.79-.465 2.428a4.883 4.883 0 0 1-1.153 1.772 4.915 4.915 0 0 1-1.772 1.153c-.637.247-1.363.415-2.428.465-1.066.048-1.405.06-4.122.06-2.717 0-3.056-.01-4.122-.06-1.065-.05-1.79-.218-2.428-.465a4.89 4.89 0 0 1-1.772-1.153 4.904 4.904 0 0 1-1.153-1.772c-.248-.637-.415-1.363-.465-2.428C2.012 15.056 2 14.717 2 12c0-2.717.01-3.056.06-4.122.05-1.066.217-1.79.465-2.428a4.88 4.88 0 0 1 1.153-1.772A4.897 4.897 0 0 1 5.45 2.525c.638-.248 1.362-.415 2.428-.465C8.944 2.012 9.283 2 12 2Zm0 5a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm6.5-.25a1.25 1.25 0 0 0-2.5 0 1.25 1.25 0 0 0 2.5 0ZM12 9a3 3 0 1 1 0 6 3 3 0 0 1 0-6Z" />
    </svg>
  ),
  linkedin: (props) => (
    <svg
      aria-hidden="true"
      fill="currentColor"
      height="24"
      viewBox="0 0 24 24"
      width="24"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path d="M6.94 5a2 2 0 1 1-4-.002 2 2 0 0 1 4 .002ZM7 8.48H3v11.5h4V8.48Zm6.32 0H9.34v11.5h3.94v-6.04c0-3.66 4.77-4 4.77 0v6.04H22v-7.4c0-6.17-7.06-5.94-8.72-2.91l.04-1.19Z" />
    </svg>
  ),
};
```

### `registry/default/sections/_types/common.ts`

```ts
/**
 * Shapes shared by every token-based section. Each section ships this file
 * alongside its own category types, so installing several sections simply
 * rewrites the same file.
 */

/** Which band a section sits on. A style choice, never part of `content`. */
export type Surface = "default" | "muted" | "primary" | "secondary";

export type ImageAsset = {
  alt: string;
  src: string;
};

export type LinkAction = {
  href: string;
  label: string;
};

/** Eyebrow + heading + optional lede, the header most sections open with. */
export type SectionIntro = {
  description?: string;
  eyebrow: string;
  title: string;
};

/** A figure that counts up when it scrolls into view. */
export type Stat = {
  decimals?: number;
  label: string;
  prefix?: string;
  suffix?: string;
  value: number;
};

/** A figure that is displayed as written ("1,240+", "4.9/5"). */
export type Highlight = {
  label: string;
  value: string;
};

export type FormField = {
  label: string;
  name: string;
  /** Only for `type: "select"`. The first option is the default. */
  options?: string[];
  placeholder?: string;
  required?: boolean;
  /** `full` spans both columns of a two-column form. */
  span?: "full" | "half";
  type?: "email" | "select" | "tel" | "text" | "textarea";
};

export type FormContent = {
  description?: string;
  fields: FormField[];
  note?: string;
  submitLabel: string;
  success: {
    description: string;
    resetLabel: string;
    title: string;
  };
  title: string;
};
```

### `registry/default/sections/_types/footer.ts`

```ts
import type { LinkAction } from "./common";

export type SocialNetwork = "facebook" | "instagram" | "linkedin";

export type FooterContent = {
  brand: {
    description: string;
    name: string;
  };
  columns: {
    links: LinkAction[];
    title: string;
  }[];
  contact?: {
    email?: { address: string; label: string };
    locations: { address: string; label: string; phone?: string }[];
  };
  copyright: string;
  credit?: LinkAction & { prefix: string };
  legal: LinkAction[];
  newsletter?: {
    buttonLabel: string;
    eyebrow: string;
    note?: string;
    placeholder: string;
  };
  socials?: {
    href: string;
    label: string;
    network: SocialNetwork;
  }[];
  socialsTitle?: string;
};
```
