Tailwind Footer Design: 4-Column Link Layout with Newsletter
Build a production-ready Tailwind footer with a 4-column link grid and newsletter form. Real code, responsive breakpoints, and dark mode — no fluff.
Why Footer Design Is Harder Than It Looks
Honestly, footers are where most component libraries phone it in. You get a single row of links, maybe a copyright line, and a logo slapped on the left. That's not a footer — that's surrender.
A real site footer does a lot of work. It's the last thing a visitor sees before they bounce, the fallback navigation for anyone who scrolled past your nav, and on SaaS products it's where your legal links live so your lawyers stop emailing you. Getting it wrong costs you SEO link equity and conversion.
In this article we're building a proper 4-column footer in Tailwind v4.0.2 with a newsletter subscribe form baked in. We'll cover the grid, responsive stacking, dark mode token wiring, and the email input pattern that doesn't embarrass you on mobile.
The Base Grid Structure for a 4-Column Tailwind Footer
The grid is the foundation. You want four equal columns on desktop, two on tablet, one on mobile — and you want that to happen with zero media query boilerplate. Tailwind's grid-cols-* utilities handle this cleanly with a single responsive string.
Here's the full structural shell. Notice the gap-x-8 gap-y-12 split — 32px horizontal breathing room between columns, 48px vertical gap when columns stack. Those aren't arbitrary numbers; they came from testing on a 320px viewport where tighter values made the stacked columns feel like one blob.
// components/SiteFooter.tsx
import { ReactNode } from 'react';
interface FooterColumn {
heading: string;
links: { label: string; href: string }[];
}
const columns: FooterColumn[] = [
{
heading: 'Product',
links: [
{ label: 'Components', href: '/components' },
{ label: 'Templates', href: '/templates' },
{ label: 'Changelog', href: '/changelog' },
{ label: 'Roadmap', href: '/roadmap' },
],
},
{
heading: 'Resources',
links: [
{ label: 'Documentation', href: '/docs' },
{ label: 'Blog', href: '/blog' },
{ label: 'Figma Kit', href: '/figma' },
{ label: 'GitHub', href: 'https://github.com' },
],
},
{
heading: 'Company',
links: [
{ label: 'About', href: '/about' },
{ label: 'Pricing', href: '/pricing' },
{ label: 'Contact', href: '/contact' },
{ label: 'Press', href: '/press' },
],
},
{
heading: 'Legal',
links: [
{ label: 'Privacy Policy', href: '/privacy' },
{ label: 'Terms of Service', href: '/terms' },
{ label: 'Cookie Policy', href: '/cookies' },
{ label: 'GDPR', href: '/gdpr' },
],
},
];
export function SiteFooter() {
return (
<footer className="bg-gray-950 text-gray-400">
<div className="mx-auto max-w-7xl px-6 py-16 lg:px-8">
{/* Top section: logo + newsletter */}
<div className="mb-16 grid grid-cols-1 gap-8 lg:grid-cols-2">
<BrandBlock />
<NewsletterBlock />
</div>
{/* Link columns */}
<div className="grid grid-cols-2 gap-x-8 gap-y-12 sm:grid-cols-2 md:grid-cols-4">
{columns.map((col) => (
<FooterColumn key={col.heading} column={col} />
))}
</div>
{/* Bottom bar */}
<div className="mt-16 border-t border-white/10 pt-8">
<p className="text-sm text-gray-600">
© {new Date().getFullYear()} Empire UI. MIT License.
</p>
</div>
</div>
</footer>
);
}Building Each Link Column with Tailwind Typography Utilities
Each column needs a heading that visually separates from the links below it. A common mistake is using a larger font size for the heading — that creates a visual hierarchy problem on mobile where large headings dominate. Better to use font weight and letter-spacing instead. text-sm font-semibold tracking-widest uppercase text-white reads clearly at any size without taking up extra height.
Link hover states are where footers often get lazy. hover:text-white transition-colors duration-150 is fine, but adding hover:translate-x-0.5 gives a subtle nudge that signals interactivity without feeling like a 2012 jQuery plugin. Keep the transition duration under 200ms — anything slower feels sluggish.
function FooterColumn({ column }: { column: FooterColumn }) {
return (
<div>
<h3 className="text-sm font-semibold uppercase tracking-widest text-white">
{column.heading}
</h3>
<ul className="mt-4 space-y-3">
{column.links.map((link) => (
<li key={link.label}>
<a
href={link.href}
className="inline-block text-sm text-gray-400 transition-all duration-150
hover:translate-x-0.5 hover:text-white"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
);
}Newsletter Form That Actually Works on Mobile
The newsletter block is where most footers fall apart on small screens. A side-by-side input + button layout at 320px viewport width produces an input field that's maybe 140px wide. Users can't see what they're typing. Don't do that.
The fix is a flex column layout on mobile that flips to a row on sm: breakpoint — flex flex-col sm:flex-row gap-3. The button gets shrink-0 so it doesn't compress when the input expands. That's it. Two classes and you've fixed a problem that plagues 80% of site footers.
function NewsletterBlock() {
return (
<div className="lg:pl-8">
<h3 className="text-sm font-semibold uppercase tracking-widest text-white">
Stay in the loop
</h3>
<p className="mt-3 text-sm text-gray-400">
New components, design tokens, and Tailwind patterns — straight to your inbox.
No spam, unsubscribe any time.
</p>
<form
onSubmit={(e) => e.preventDefault()}
className="mt-5 flex flex-col gap-3 sm:flex-row"
>
<label htmlFor="footer-email" className="sr-only">
Email address
</label>
<input
id="footer-email"
type="email"
required
placeholder="you@example.com"
className="
w-full rounded-lg border border-white/10 bg-white/5
px-4 py-2.5 text-sm text-white placeholder-gray-500
outline-none ring-1 ring-transparent
transition-colors duration-150
focus:border-indigo-500 focus:ring-indigo-500/30
"
/>
<button
type="submit"
className="
shrink-0 rounded-lg bg-indigo-600 px-5 py-2.5
text-sm font-medium text-white
transition-colors duration-150
hover:bg-indigo-500 active:scale-95
"
>
Subscribe
</button>
</form>
</div>
);
}The bg-white/5 on the input and border-white/10 on its border are Tailwind's opacity modifier syntax — shorthand for rgba(255,255,255,0.05) and rgba(255,255,255,0.10) respectively. These values come from testing against dark backgrounds; anything below 0.05 becomes invisible, anything above 0.15 starts looking like a light-mode input dropped on a dark page.
Dark Mode Token Wiring with Tailwind v4 CSS Variables
If your site has a theme toggle, your footer needs to respond to it. Hardcoding bg-gray-950 works fine for a dark-only footer, but the moment you add a light mode you're touching every class. In Tailwind v4.0.2, the CSS custom property system makes this straightforward.
Define your footer surface and text tokens once in your CSS layer, then reference them with bg-[--footer-bg] in your components. The dark: variant flips the variable value — nothing in the JSX changes. This pattern aligns with Tailwind v4's new design token features and keeps your component tree clean.
/* globals.css */
@layer base {
:root {
--footer-bg: theme('colors.gray.100');
--footer-border: theme('colors.gray.200');
--footer-text: theme('colors.gray.500');
--footer-heading: theme('colors.gray.900');
--footer-link-hover: theme('colors.gray.900');
}
.dark {
--footer-bg: theme('colors.gray.950');
--footer-border: rgba(255, 255, 255, 0.08);
--footer-text: theme('colors.gray.400');
--footer-heading: theme('colors.white');
--footer-link-hover: theme('colors.white');
}
}Then in the component: className="bg-[--footer-bg] text-[--footer-text]". When the user toggles dark mode, the variables cascade and every footer element repaints. No duplicate class sets, no JS logic in the component, no conditional classnames.
Responsive Stacking and Column Order on Small Screens
Four columns stacking into one on mobile is fine, but the order matters. On a 375px phone, your visitors see the top column first and have to scroll to find the rest. If "Legal" is column four on desktop, it should probably stay last on mobile too — but "Product" links (your primary nav fallback) should be first.
With CSS Grid, the DOM order controls stacking order by default. So just arrange your columns array with the most important category first. What about the brand block? It should always sit above the columns, which the mb-16 on the top section handles.
One thing worth checking: do your two-column mobile columns balance visually if one column has three links and another has six? They won't align. Either normalize link counts across columns or accept the ragged bottom edge — it's not broken, it just looks more intentional if you pick a side. If you want consistent column heights, check out how Tailwind container queries can help you adapt column count based on available width rather than viewport breakpoints.
Social Icons and Brand Block in the Footer Header
The brand block sits top-left and typically includes a logo, a one-liner tagline, and social icon links. Keep the tagline under 12 words. Nobody reads footer taglines — they're there for brand consistency, not conversion. A text-sm text-gray-400 mt-3 max-w-xs keeps it from stretching grotesquely on wide viewports.
Social icons in footers should be 20px icons with 32px click targets — that's the p-1.5 padding trick on the <a> wrapper. Use aria-label on every social link since the icon itself carries no text. Screen readers will thank you, and your accessibility audit will stop flagging it.
For icon coloring, text-gray-500 hover:text-white transition-colors duration-150 works on dark backgrounds. If you're exploring more advanced visual styles like glassmorphism effects for the footer container, you can layer a backdrop-blur-sm bg-white/5 border border-white/10 rounded-2xl around the entire footer section for a frosted look — though that pattern works better on hero sections than footers with dense link grids.
Accessibility and SEO Considerations for Footer Link Grids
Wrap your footer in a <footer> landmark and add aria-label="Site footer" so screen reader users can jump to it directly. The <nav> element inside each column is optional but helps — <nav aria-label="Product links"> creates a named landmark for each section. Does your current footer do this? Probably not. It takes 30 seconds to add.
For SEO, footer links pass PageRank internally. Four columns of 4 links each is 16 internal links — that's fine. Going beyond 30 footer links starts to dilute the signal and can look spammy to crawlers. Also make sure every footer link has descriptive anchor text. "Click here" and "Learn more" in a footer are wasted equity. Concrete text like "Tailwind Component Patterns" tells both users and crawlers exactly what they're getting, just like we reference in our component patterns guide.
Finally, test your footer with keyboard navigation. Tab through every link and the email input. The focus ring on the input — focus:border-indigo-500 focus:ring-indigo-500/30 — should be visually distinct. If you've used outline-none without replacing the focus indicator with something visible, you've broken keyboard accessibility. That's not a warning, it's a fact.
FAQ
Use grid grid-cols-2 md:grid-cols-4 on the column wrapper. On screens below the md breakpoint (768px by default in Tailwind), you get 2 columns. On md and above you get 4. No custom breakpoints needed.
Use a Server Action. Add 'use server' to an async function, pass it to <form action={yourAction}>, and handle the email with Resend or your ESP's API. This avoids client-side JS for the form entirely and gives you progressive enhancement for free.
Each column of links benefits from a wrapping <nav aria-label="Column name"> for accessibility. Screen reader users can then jump directly to a named navigation region rather than tabbing through every link. The <ul> inside is still correct — nav doesn't replace lists, it wraps them.
Set text-sm which maps to 14px. iOS Safari zooms inputs when font-size is below 16px. Either use text-base (16px) on the input, or add touch-action: manipulation in your global styles. The Tailwind class text-[16px] as an arbitrary value also works directly on the input element.
Yes. Tailwind v4 uses oklch natively for its color palette. You can set --footer-bg: oklch(0.12 0.01 265) in your CSS variables for a near-black with a slight blue-gray tint. Check the oklch color space article for a full token setup guide.
There's no hard cutoff, but staying under 30 total footer links is a reasonable rule. Every link in a footer is a vote you're casting for that destination page. Spreading that across 50+ links means each link carries less signal. Four columns of four links (16 total) is well within safe territory.