# Site footer (Caseroom block)

Brand, link columns and a legal row; on the page or as an inverted band that closes it. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

85% of the 2,062 sites in The UI Index have a footer (1,753 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/site-footer.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/site-footer.json`
- HTML: https://www.themes.dev/caseroom/r/html/site-footer.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/blocks/site-footer/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### site-footer.astro

```astro
---
import { cn } from '@/lib/utils';
import { siteFooter as c, type FooterContent } from './site-footer.classes';

type Props = FooterContent & { class?: string };
const { brand, tagline, groups, legal, note, variant = 'columns', class: className } = Astro.props;
---

<footer class={cn(c.root, variant === 'band' && c.band, className)}>
  <div class={c.inner}>
    <div class={c.top}>
      <div class={c.brand}>
        <p class={c.brandName}>{brand}</p>
        {tagline && <p class={c.tagline}>{tagline}</p>}
      </div>
      <div class={c.groups}>
        {
          groups.map((g) => (
            <nav aria-label={g.title} class={c.group}>
              <p class={c.groupTitle}>{g.title}</p>
              <ul class={c.groupList}>
                {g.links.map((l) => (
                  <li><a href={l.href} class={c.link}>{l.label}</a></li>
                ))}
              </ul>
            </nav>
          ))
        }
      </div>
    </div>
    <div class={c.legal}>
      <p>{note}</p>
      <ul class={c.legalList} aria-label="Legal">
        {legal.map((l) => <li><a href={l.href} class={c.link}>{l.label}</a></li>)}
      </ul>
    </div>
  </div>
</footer>
```

### site-footer.classes.ts

```tsx
/** The site footer (ported from the themes' sections/footer): brand and a line about the product, link columns,
 *  and a legal row. columns sits on the page's ground with space above; band is an inverted ground (class
 *  inverted from the theme), the page's closing bookend. Links are 32px targets. */
export const siteFooter = {
  root: '@container bg-background px-6 pt-16 pb-10 text-foreground',
  band: 'inverted',
  inner: 'mx-auto flex max-w-6xl flex-col gap-12',
  top: 'grid gap-12 @3xl:grid-cols-[1.2fr_2fr]',
  brand: 'flex flex-col gap-3',
  brandName: 'text-[15px] font-semibold tracking-[-0.02em]',
  tagline: 'max-w-xs text-sm text-pretty text-muted-foreground',
  // Three columns at every width: two would leave the third group alone on its row.
  groups: 'grid grid-cols-3 gap-4 @md:gap-6 @lg:gap-8 [&>*]:min-w-0',
  group: 'flex flex-col gap-2',
  groupTitle: 'text-sm font-medium',
  groupList: 'm-0 flex list-none flex-col p-0',
  link: '-mx-1 inline-flex min-h-8 items-center rounded-sm px-1 text-sm text-muted-foreground transition-colors hover:text-foreground hover:underline hover:underline-offset-4 active:text-foreground/70',
  legal: 'flex flex-col gap-3 border-t pt-6 text-sm text-muted-foreground @xl:flex-row @xl:items-center @xl:justify-between',
  legalList: 'm-0 flex list-none flex-wrap gap-x-5 p-0',
};

export type FooterGroup = { title: string; links: { label: string; href: string }[] };
export type FooterContent = {
  brand: string;
  tagline?: string;
  groups: FooterGroup[];
  legal: { label: string; href: string }[];
  /** The small print, e.g. "© 2026 Northwind". */
  note: string;
  variant?: 'columns' | 'band';
};
```

### site-footer.tsx

```tsx
import { cn } from '@/lib/utils';
import { siteFooter as c, type FooterContent } from './site-footer.classes';

export type SiteFooterProps = FooterContent & { className?: string };

/** The site footer: brand, link columns and a legal row; band puts it on an inverted ground. */
export function SiteFooter({ brand, tagline, groups, legal, note, variant = 'columns', className }: SiteFooterProps) {
  return (
    <footer className={cn(c.root, variant === 'band' && c.band, className)}>
      <div className={c.inner}>
        <div className={c.top}>
          <div className={c.brand}>
            <p className={c.brandName}>{brand}</p>
            {tagline && <p className={c.tagline}>{tagline}</p>}
          </div>
          <div className={c.groups}>
            {groups.map((g) => (
              <nav key={g.title} aria-label={g.title} className={c.group}>
                <p className={c.groupTitle}>{g.title}</p>
                <ul className={c.groupList}>
                  {g.links.map((l) => (
                    <li key={l.href + l.label}><a href={l.href} className={c.link}>{l.label}</a></li>
                  ))}
                </ul>
              </nav>
            ))}
          </div>
        </div>
        <div className={c.legal}>
          <p>{note}</p>
          <ul className={c.legalList} aria-label="Legal">
            {legal.map((l) => <li key={l.href + l.label}><a href={l.href} className={c.link}>{l.label}</a></li>)}
          </ul>
        </div>
      </div>
    </footer>
  );
}
```
