UI.MD

Site Footer

The shared site chrome footer: a shell plus parts, from a one-line strip to a multi-column footer.

Installation

$
npx shadcn@latest add https://ui.mattdowney.com/r/site-footer.json

Usage

import {
  SiteFooter, SiteFooterBrand, SiteFooterColumn, SiteFooterColumns,
  SiteFooterFinePrint, SiteFooterRow,
  siteFooterNavItemClass, siteFooterIconLinkClass,
} from "@/components/ui/site-footer"

Examples

Default strip

Pass no children and the shell renders the original single-row strip: attribution and copyright on one side, links on the other.

Page title

Page content ends here and the footer closes the column.

Multi-column

Brand on one side, the column region on the other, then a bottom bar. SiteFooterColumns puts the columns on equal grid tracks: packed into a flex row they size to their own content, so nothing lines up, and justify-between flings them to the edges. Two columns below sm, one track per column above it.

Page title

Page content ends here and the footer closes the column.

Centred

align="center" stacks the whole footer down the middle: the archetype for a small site or a single project page, where a start-aligned footer with one short link row looks abandoned in the corner. It reaches the parts through a scoped CSS rule rather than React context, so the footer stays a Server Component.

Page title

Page content ends here and the footer closes the column.

Inside a padded page container

The two props for a footer that is not mounted full width. bleed moves the top rule onto a full-width child that escapes the page container's padding, so it meets the page's real edges. Extra height goes in contentClassName: className lands on the outer box and never reaches the padding that sets it.

Page title

The page column pads itself, and the footer sits inside it.

Without the kit's credit

attribution={false} drops the "built by Matt Downey using UI.MD" line and keeps the copyright, which is what a product with its own company name wants. The separator goes with the credit, so the line never starts with a stray one. Pass a node instead of false to substitute your own credits and keep the year.

Page title

Page content ends here and the footer closes the column.

API Reference

PropTypeDefaultDescription
childrenSiteFooterReactNode-Rows, brand blocks, and columns. Wins over the strip props.
nameSiteFooter, SiteFooterFinePrint, SiteFooterAttributionstring"UI"The site's wordmark name for the default attribution, e.g. "SKILLS". The "using UI.MD" clause is dropped when the site is UI.MD itself.
attributionSiteFooter, SiteFooterFinePrintReactNode | false-Replaces the default credits. The copyright still follows it, so restating credits never drops the year. Pass false to drop the credits entirely and leave the copyright on its own, which is what a product outside the mattdowney.com family wants: the separator goes with them, so you never get a line starting with a stray ·.
navSiteFooterReactNode-Strip links, styled with siteFooterNavItemClass.
justifySiteFooterRow"between" | "start" | "end""between"How the run distributes its children. between is what a footer row nearly always wants; the other two keep it packed to one side. Ignored under align="center", which centres every row.
dividerSiteFooterRowbooleanfalseA hairline above the row, for the legal bar under a tall footer. The line lands in the shell's own gap-8 and the row's padding matches it, so the rule sits evenly between the two zones instead of hugging one.
alignSiteFooter"start" | "center""start"center stacks the whole footer down the middle, reaching the parts through a scoped CSS rule rather than React context so the footer stays a Server Component.
titleSiteFooterColumnReactNode-Column heading above the links, rendered as the kit's group label: uppercase, tracking-wide, semibold, in text-foreground over muted links. Omit for an untitled stack.
childrenSiteFooterColumnsReactNode-Two or more <SiteFooterColumn />. They land on equal grid tracks, two-up below sm and one track each above it, however many you pass. Packed into a flex row instead they would size to their own content and fail to line up.
bleedSiteFooterboolean | stringfalseWhere the top rule is drawn. The default is the full-width mount: the border sits on the <footer>, which is correct when the footer already spans the viewport. Set it for a footer inside a page container that pads itself, and the rule moves to a full-width child that escapes the padding by a negative inline margin, so it meets the page's real edges while nothing in the footer moves. It stays inside the <footer> either way. true reads the distance from --spacing-chrome-bleed (which a responsive container redeclares at its own breakpoint); a string states it directly.
containerSiteFooterbooleantrueWhether the shell builds its own centred container. On by default, which is right when the footer spans the viewport. Set it to false when the footer already sits inside a page container that centres and pads, or the two nest and the footer's contents sit inboard of the rest of the page by a full gutter. Doing it by neutralising --container-chrome and --spacing-chrome-gutter instead is global and moves every shell on the site; this is per-instance. The shell still owns the surface, the edge, and the content box's padding and gap.
classNameSiteFooter, SiteFooterRow, SiteFooterBrand, SiteFooterColumn, SiteFooterColumnsstring-On <SiteFooter /> it is the outer box: border, background, page-level margin. Pass mt-auto there when the page is a flex min-h-dvh flex-col column. Padding and type are contentClassName, not this.
contentClassNameSiteFooterstring-The inner content box: padding, gap, type. This is where a footer taller than the default py-8 goes, since className never reaches it. Merged after the built-in classes, so it wins.