Footer

Site footer with brand, links and legal line

Footer closes a site: brand and tagline, columns of links, and a bottom bar for copyright, legal links and social icons. It renders a footer element, and simple sites can swap the columns for a single row of links. The home page ends with one.

Installation

Install the dependencies.

npm install @kushagradhawan/kookie-ui

Copy this file into components/blocks/footer. Keep the folder names, since blocks import each other by relative path.

components/blocks/footer/footer.tsx
import * as React from 'react';
import { Flex, Heading, IconButton, Link, Text } from '@kushagradhawan/kookie-ui';
import type { FlexProps, IconButtonProps, LinkProps, TextProps } from '@kushagradhawan/kookie-ui';

type FooterRootProps = Omit<FlexProps, 'align'> & {
  /** @default "left" */
  align?: 'left' | 'center';
};

const FooterRoot = React.forwardRef<HTMLDivElement, FooterRootProps>(
  ({ align = 'left', direction = 'column', gap = '6', children, ...props }, ref) => (
    <Flex ref={ref} asChild direction={direction} align={align === 'center' ? 'center' : 'stretch'} gap={gap} {...props}>
      <footer>{children}</footer>
    </Flex>
  ),
);
FooterRoot.displayName = 'Footer.Root';

/** Top section: brand on one side, link groups on the other. */
const FooterMain = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ gap = '8', justify = 'between', align = 'start', wrap = 'wrap', ...props }, ref) => (
    <Flex ref={ref} gap={gap} justify={justify} align={align} wrap={wrap} {...props} />
  ),
);
FooterMain.displayName = 'Footer.Main';

/** Bottom bar for legal text and social links. */
const FooterBottom = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ gap = '4', justify = 'between', align = 'center', wrap = 'wrap', ...props }, ref) => (
    <Flex ref={ref} gap={gap} justify={justify} align={align} wrap={wrap} {...props} />
  ),
);
FooterBottom.displayName = 'Footer.Bottom';

const FooterBrand = React.forwardRef<HTMLDivElement, FlexProps>(({ direction = 'column', gap = '3', ...props }, ref) => (
  <Flex ref={ref} direction={direction} gap={gap} {...props} />
));
FooterBrand.displayName = 'Footer.Brand';

const FooterBrandName = React.forwardRef<HTMLSpanElement, TextProps>(({ size = '4', weight = 'bold', ...props }, ref) => (
  <Text ref={ref} size={size} weight={weight} {...props} />
));
FooterBrandName.displayName = 'Footer.BrandName';

const FooterTagline = React.forwardRef<HTMLSpanElement, TextProps>(({ size = '2', emphasis = 'medium', ...props }, ref) => (
  <Text ref={ref} size={size} emphasis={emphasis} {...props} />
));
FooterTagline.displayName = 'Footer.Tagline';

/** Holds several `Footer.LinkGroup` columns. */
const FooterLinks = React.forwardRef<HTMLDivElement, FlexProps>(({ gap = '8', wrap = 'wrap', ...props }, ref) => (
  <Flex ref={ref} gap={gap} wrap={wrap} {...props} />
));
FooterLinks.displayName = 'Footer.Links';

type FooterLinkGroupProps = FlexProps & { title: string };

const FooterLinkGroup = React.forwardRef<HTMLDivElement, FooterLinkGroupProps>(
  ({ title, direction = 'column', gap = '3', children, ...props }, ref) => (
    <Flex ref={ref} direction={direction} gap={gap} {...props}>
      <Heading as="h3" size="2" weight="medium">
        {title}
      </Heading>
      <Flex direction="column" gap="2">
        {children}
      </Flex>
    </Flex>
  ),
);
FooterLinkGroup.displayName = 'Footer.LinkGroup';

/** A single row of links, for footers without groups. */
const FooterNav = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ gap = '6', align = 'center', wrap = 'wrap', children, ...props }, ref) => (
    <Flex ref={ref} asChild gap={gap} align={align} wrap={wrap} {...props}>
      <nav>{children}</nav>
    </Flex>
  ),
);
FooterNav.displayName = 'Footer.Nav';

const FooterLink = React.forwardRef<HTMLAnchorElement, LinkProps>(({ size = '2', color = 'gray', ...props }, ref) => (
  <Link ref={ref} size={size} color={color} {...props} />
));
FooterLink.displayName = 'Footer.Link';

/** Copyright line and legal links. */
const FooterLegal = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ gap = '4', align = 'center', wrap = 'wrap', ...props }, ref) => (
    <Flex ref={ref} gap={gap} align={align} wrap={wrap} {...props} />
  ),
);
FooterLegal.displayName = 'Footer.Legal';

const FooterSocial = React.forwardRef<HTMLDivElement, FlexProps>(({ gap = '4', align = 'center', ...props }, ref) => (
  <Flex ref={ref} gap={gap} align={align} {...props} />
));
FooterSocial.displayName = 'Footer.Social';

type FooterSocialLinkProps = Omit<IconButtonProps, 'children' | 'aria-label' | 'aria-labelledby' | 'asChild'> & {
  /** The icon to render, e.g. an SVG component. */
  icon: React.ComponentType;
  /** Accessible name, e.g. "GitHub". */
  label: string;
  href: string;
};

const FooterSocialLink = React.forwardRef<HTMLButtonElement, FooterSocialLinkProps>(
  ({ icon: Icon, label, href, variant = 'ghost', color = 'gray', size = '2', ...props }, ref) => (
    <IconButton ref={ref} asChild variant={variant} color={color} size={size} aria-label={label} {...props}>
      <a href={href} target="_blank" rel="noopener noreferrer">
        <Icon />
      </a>
    </IconButton>
  ),
);
FooterSocialLink.displayName = 'Footer.SocialLink';

export const Footer = Object.assign(FooterRoot, {
  Root: FooterRoot,
  Main: FooterMain,
  Bottom: FooterBottom,
  Brand: FooterBrand,
  BrandName: FooterBrandName,
  Tagline: FooterTagline,
  Links: FooterLinks,
  LinkGroup: FooterLinkGroup,
  Nav: FooterNav,
  Link: FooterLink,
  Legal: FooterLegal,
  Social: FooterSocial,
  SocialLink: FooterSocialLink,
});

Usage

import { Footer } from '@/components/blocks/footer/footer';
 
<Footer.Root>
  <Footer.Main>
    <Footer.Brand>
      <Footer.BrandName>Acme</Footer.BrandName>
      <Footer.Tagline>Tools for small teams.</Footer.Tagline>
    </Footer.Brand>
    <Footer.Links>
      <Footer.LinkGroup title="Product">
        <Footer.Link href="/pricing">Pricing</Footer.Link>
        <Footer.Link href="/changelog">Changelog</Footer.Link>
      </Footer.LinkGroup>
    </Footer.Links>
  </Footer.Main>
  <Footer.Bottom>
    <Footer.Legal>
      <Text size="2" emphasis="medium">© 2026 Acme</Text>
    </Footer.Legal>
  </Footer.Bottom>
</Footer.Root>

Anatomy

<Footer.Root>
  <Footer.Main>
    <Footer.Brand>
      <Footer.BrandName />
      <Footer.Tagline />
    </Footer.Brand>
    <Footer.Links>
      <Footer.LinkGroup title="…">
        <Footer.Link />
      </Footer.LinkGroup>
    </Footer.Links>
  </Footer.Main>
  <Footer.Nav>
    <Footer.Link />
  </Footer.Nav>
  <Footer.Bottom>
    <Footer.Legal />
    <Footer.Social>
      <Footer.SocialLink />
    </Footer.Social>
  </Footer.Bottom>
</Footer.Root>

Props

Every part accepts the props of the component it wraps. Only the additions and changed defaults are listed.

PartWrapsAdds or changes
RootFlex as footeralign?: 'left' | 'center' ('left'). direction="column", gap="6"
MainFlexgap="8", justify="between", align="start", wrap="wrap"
BrandFlexdirection="column", gap="3"
BrandNameTextsize="4", weight="bold"
TaglineTextsize="2", emphasis="medium"
LinksFlexgap="8", wrap="wrap"
LinkGroupFlextitle: string, rendered as an h3. direction="column", gap="3"
NavFlex as navgap="6", align="center", wrap="wrap"
LinkLinksize="2", color="gray"
BottomFlexgap="4", justify="between", align="center", wrap="wrap"
LegalFlexgap="4", align="center", wrap="wrap"
SocialFlexgap="4", align="center"
SocialLinkIconButtonicon: ComponentType, label: string, href: string. variant="ghost", color="gray", size="2". Opens in a new tab

Examples

<Footer.Root>
  <Footer.Main>
    <Footer.Brand>
      <Footer.BrandName>Acme</Footer.BrandName>
      <Footer.Tagline>Tools for small teams.</Footer.Tagline>
    </Footer.Brand>
    <Footer.Links>
      <Footer.LinkGroup title="Product">
        <Footer.Link href="/pricing">Pricing</Footer.Link>
        <Footer.Link href="/changelog">Changelog</Footer.Link>
      </Footer.LinkGroup>
      <Footer.LinkGroup title="Company">
        <Footer.Link href="/about">About</Footer.Link>
        <Footer.Link href="/careers">Careers</Footer.Link>
      </Footer.LinkGroup>
    </Footer.Links>
  </Footer.Main>
  <Separator size="4" />
  <Footer.Bottom>
    <Footer.Legal>
      <Text size="2" emphasis="medium">© 2026 Acme Inc.</Text>
      <Footer.Link href="/privacy">Privacy</Footer.Link>
      <Footer.Link href="/terms">Terms</Footer.Link>
    </Footer.Legal>
  </Footer.Bottom>
</Footer.Root>

SocialLink takes an icon component. With HugeIcons, wrap the icon so the button controls its size.

import { HugeiconsIcon } from '@hugeicons/react';
import { GithubIcon, NewTwitterIcon } from '@hugeicons/core-free-icons';
 
const GitHub = () => <HugeiconsIcon icon={GithubIcon} strokeWidth={1.75} />;
const X = () => <HugeiconsIcon icon={NewTwitterIcon} strokeWidth={1.75} />;
 
<Footer.Social>
  <Footer.SocialLink icon={GitHub} label="GitHub" href="https://github.com/acme" />
  <Footer.SocialLink icon={X} label="X" href="https://x.com/acme" />
</Footer.Social>

Footer.Link wraps Kookie's Link, so asChild works with your router.

<Footer.Link asChild>
  <NextLink href="/pricing">Pricing</NextLink>
</Footer.Link>

Accessibility

Root is a footer landmark and Nav is a nav landmark. Give Nav an aria-label, such as "Footer", when the page has other navigation. Every SocialLink needs a label, which becomes its accessible name.

© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background