liten

Footer

A giant-wordmark SaaS footer with a vivid accent band of tinted dots and a brand wordmark that rises in letter-by-letter and clips at the seam.

A vivid accent band of tinted dots holds a centered tagline and an oversized brand wordmark that rises in letter-by-letter and is clipped where the band melts into the link block below. Built with Motion.

Installation

Complete the shared Setup first, then copy the component into components/ui/footer.tsx.

Terminal
bun add motion
components/ui/footer.tsx
'use client';import * as React from 'react';import { motion, useReducedMotion, type Variants } from 'motion/react';import { cn } from '@/lib/cn';const EASE_OUT = [0.23, 1, 0.32, 1] as const;export type FooterLink = { label: string; href: string };export type FooterColumn = { title: string; links: FooterLink[] };export type FooterProps = {  brand: string;  tagline?: string;  columns: FooterColumn[];  social?: FooterLink[];  copyright?: string;  accent?: string;  className?: string;};export function Footer({  brand,  tagline,  columns,  social,  copyright,  accent = '#3b82f6',  className,}: FooterProps) {  const reduce = useReducedMotion();  const go = true;  const rise: Variants = {    hidden: { opacity: 0, y: reduce ? 0 : 14 },    show: (i: number) => ({      opacity: 1,      y: 0,      transition: { duration: 0.5, ease: EASE_OUT, delay: reduce ? 0 : i * 0.06 },    }),  };  return (    <footer      style={{ '--fa': accent } as React.CSSProperties}      className={cn(        'relative isolate overflow-hidden',        '[--band-base:#fafafa] [--surface:#f4f4f5] dark:[--band-base:#0a0a0b] dark:[--surface:#1c1c1c]',        'bg-[var(--surface)] text-neutral-900 dark:text-white',        className,      )}    >      <div className="relative overflow-hidden bg-[var(--band-base)]">        <BandDots />        {tagline ? (          <motion.p            custom={0}            variants={rise}            initial="hidden"            animate={go ? 'show' : 'hidden'}            className="relative z-10 px-6 pt-14 text-center text-[18px] font-medium tracking-[-0.01em] sm:text-[22px]"          >            {tagline}          </motion.p>        ) : (          <div className="pt-14" />        )}        <Wordmark brand={brand} go={go} reduce={!!reduce} />      </div>      <div className="mx-auto w-full max-w-6xl px-6 pb-10 pt-12">        <div className="grid grid-cols-2 gap-x-6 gap-y-10 sm:grid-cols-3 lg:grid-cols-5">          {columns.map((col, i) => (            <motion.nav              key={col.title}              aria-label={col.title}              custom={i + 1}              variants={rise}              initial="hidden"              animate={go ? 'show' : 'hidden'}              className="flex flex-col"            >              <h3 className="mb-3.5 text-[15px] font-semibold tracking-[-0.01em]">                {col.title}              </h3>              <ul className="flex flex-col gap-2.5">                {col.links.map((link) => (                  <li key={link.label}>                    <FooterAnchor href={link.href}>{link.label}</FooterAnchor>                  </li>                ))}              </ul>            </motion.nav>          ))}          {social && social.length > 0 ? (            <motion.nav              aria-label="Socials"              custom={columns.length + 1}              variants={rise}              initial="hidden"              animate={go ? 'show' : 'hidden'}              className="flex flex-col"            >              <h3 className="mb-3.5 text-[15px] font-semibold tracking-[-0.01em]">                Socials              </h3>              <ul className="flex flex-col gap-2.5">                {social.map((link) => (                  <li key={link.label}>                    <FooterAnchor href={link.href} underline>                      {link.label}                    </FooterAnchor>                  </li>                ))}              </ul>            </motion.nav>          ) : null}        </div>        {copyright ? (          <p className="mt-16 text-[13px] text-neutral-500 dark:text-[#8b8b8b]">            {copyright}          </p>        ) : null}      </div>    </footer>  );}const DOT_MASK: React.CSSProperties = {  WebkitMaskImage:    'radial-gradient(circle at center, #000 1px, transparent 1.4px)',  maskImage: 'radial-gradient(circle at center, #000 1px, transparent 1.4px)',  WebkitMaskSize: '5px 5px',  maskSize: '5px 5px',};function BandDots() {  const COLS = 44;  const mid = (COLS - 1) / 2;  return (    <div aria-hidden className="pointer-events-none absolute inset-0 z-0">      <div        className="absolute inset-0 bg-black/[0.06] dark:bg-white/[0.08]"        style={DOT_MASK}      />      <div className="footer-dot-hue absolute inset-0 flex" style={DOT_MASK}>        {Array.from({ length: COLS }).map((_, i) => {          const t = Math.abs(i - mid) / mid;          const reach = 24 + t * t * 56;          return (            <div              key={i}              className="h-full flex-1"              style={{                background: `linear-gradient(180deg, var(--fa) 0%, color-mix(in srgb, var(--fa) 55%, transparent) ${(reach * 0.5).toFixed(1)}%, transparent ${reach.toFixed(1)}%)`,              }}            />          );        })}      </div>    </div>  );}function Wordmark({  brand,  go,  reduce,}: {  brand: string;  go: boolean;  reduce: boolean;}) {  const letters = React.useMemo(() => Array.from(brand), [brand]);  const wordStyle: React.CSSProperties = {    fontSize: 'clamp(96px,23vw,380px)',    lineHeight: 0.92,    letterSpacing: '-0.045em',  };  const container: Variants = {    hidden: {},    show: { transition: { staggerChildren: reduce ? 0 : 0.05, delayChildren: 0.12 } },  };  const letter: Variants = {    hidden: reduce      ? { y: '0%', opacity: 1, scale: 1, filter: 'blur(0px)' }      : { y: '110%', opacity: 0, scale: 0.9, filter: 'blur(16px)' },    show: {      y: '0%',      opacity: 1,      scale: 1,      filter: 'blur(0px)',      transition: reduce        ? { duration: 0 }        : {            y: { type: 'spring', duration: 0.9, bounce: 0.28 },            scale: { type: 'spring', duration: 0.9, bounce: 0.28 },            opacity: { duration: 0.5, ease: EASE_OUT },            filter: { duration: 0.6, ease: EASE_OUT },          },    },  };  return (    <div      aria-hidden      className="relative z-[1] mt-4 h-[clamp(78px,17.5vw,290px)] overflow-hidden"    >      <motion.span        className="absolute left-1/2 top-0 flex -translate-x-1/2 whitespace-nowrap font-bold"        style={wordStyle}        variants={container}        initial="hidden"        animate={go ? 'show' : 'hidden'}        aria-label={brand}      >        {letters.map((ch, i) => (          <span key={i} className="inline-block overflow-hidden">            <motion.span variants={letter} className="inline-block">              {ch === ' ' ? ' ' : ch}            </motion.span>          </span>        ))}      </motion.span>    </div>  );}const FOCUS =  'focus-visible:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:[outline-color:var(--fa)]';function FooterAnchor({  href,  underline,  children,}: {  href: string;  underline?: boolean;  children: React.ReactNode;}) {  return (    <a      href={href}      className={cn(        'inline-block rounded-[4px] text-[14px] text-neutral-500 transition-colors duration-150 ease-out',        'hover:text-neutral-900 dark:text-[#929292] dark:hover:text-white',        underline && 'underline decoration-from-font underline-offset-[3px]',        FOCUS,      )}    >      {children}    </a>  );}

Add the hue drift to your global.css.

global.css
.footer-dot-hue {
  animation: footer-dot-hue 12s ease-in-out infinite;
}
@keyframes footer-dot-hue {
  0% {
    filter: hue-rotate(-28deg);
  }
  50% {
    filter: hue-rotate(34deg);
  }
  100% {
    filter: hue-rotate(-28deg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .footer-dot-hue {
    animation: none;
    filter: none;
  }
}

Usage

Everything is data: pass the brand, tagline, columns, and socials.

Example.tsx
import { Footer } from '@/components/ui/footer';

export default function Example() {
  return (
    <Footer
      brand="Liten"
      tagline="Design-engineered components. Depth, not glow."
      accent="#ff2e88"
      columns={[
        { title: 'Product', links: [{ label: 'Pricing', href: '/pricing' }] },
        { title: 'Company', links: [{ label: 'About', href: '/about' }] },
      ]}
      social={[
        { label: 'X', href: '#' },
        { label: 'Linkedin', href: '#' },
      ]}
      copyright="© 2026 Liten. Crafted with depth."
    />
  );
}

Props

PropTypeDefaultDescription
brandstring-The oversized wordmark behind the band.
taglinestring-Centered line inside the accent band.
columnsFooterColumn[]-Link columns ({ title, links }).
socialFooterLink[]-Socials column, rendered as underlined text links.
copyrightstring-Muted credit line at the very bottom.
accentstring"#ff2e88"The vivid accent the band is built from.
classNamestring-Forwarded to the <footer>.
On this page0%