Footer and Content Components Intermediate 12 min read

Footer Component with Dynamic Columns

Build a flexible footer component with React that supports dynamic column layouts. Learn how to structure footer navigation, integrate social links, and display payment icons.

The footer is a critical part of any e-commerce site—it’s where customers find important links, contact information, and trust signals like payment icons. Let’s build a flexible footer component that supports dynamic column layouts configured from the Shopify theme editor.

┌────────────────────────────────────────────────────────────────────────────────┐
│ FOOTER │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ FOOTER COLUMNS (grid) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ COLUMN 1 │ │ COLUMN 2 │ │ COLUMN 3 │ │ COLUMN 4 │ │ │
│ │ │ (links) │ │ (links) │ │ (text) │ │ (newsletter)│ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ Shop │ │ Help │ │ About Us │ │ Subscribe │ │ │
│ │ │ - All │ │ - FAQ │ │ "We are a │ │ [email ] │ │ │
│ │ │ - New │ │ - Contact │ │ family..."│ │ [Submit ] │ │ │
│ │ │ - Sale │ │ - Shipping │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ FOOTER BOTTOM │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ SOCIAL LINKS │ │ PAYMENT ICONS │ │ │
│ │ │ [fb] [ig] [tw] [yt]│ │ [visa] [mc] [amex] [paypal]│ │ │
│ │ └─────────────────────┘ └─────────────────────────────┘ │ │
│ │ │ │
│ │ © 2026 Store Name. All rights reserved. │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────┘
src/components/footer/Footer/Footer.tsx
import { FooterColumn } from './FooterColumn';
import { SocialLinks } from './SocialLinks';
import { PaymentIcons } from './PaymentIcons';
import styles from './Footer.module.css';
/**
* Footer data structure passed from Liquid.
*/
interface FooterData {
columns: FooterColumnData[];
socialLinks: SocialLink[];
paymentIcons: string[];
copyrightText: string;
showPaymentIcons: boolean;
}
interface FooterColumnData {
type: 'links' | 'text' | 'newsletter';
title: string;
links?: { label: string; url: string }[];
text?: string;
}
interface SocialLink {
platform: string;
url: string;
}
interface FooterProps {
data: FooterData;
}
/**
* Footer is the main site footer component.
* Features:
* - Dynamic column layout (1-4 columns)
* - Multiple column types: links, text, newsletter
* - Social links with icons
* - Payment method icons
* - Responsive grid layout
*/
export function Footer({ data }: FooterProps) {
const { columns, socialLinks, paymentIcons, copyrightText, showPaymentIcons } = data;
return (
<footer className={styles.footer} role="contentinfo">
{/* Main footer content */}
<div className={styles.container}>
{/* Footer columns grid */}
<div
className={styles.columns}
style={{ '--column-count': columns.length } as React.CSSProperties}
>
{columns.map((column, index) => (
<FooterColumn key={index} data={column} />
))}
</div>
{/* Footer bottom section */}
<div className={styles.bottom}>
<div className={styles.bottomLeft}>
{/* Social links */}
{socialLinks.length > 0 && <SocialLinks links={socialLinks} />}
</div>
<div className={styles.bottomCenter}>
{/* Copyright text */}
<p className={styles.copyright}>{copyrightText}</p>
</div>
<div className={styles.bottomRight}>
{/* Payment icons */}
{showPaymentIcons && paymentIcons.length > 0 && (
<PaymentIcons icons={paymentIcons} />
)}
</div>
</div>
</div>
</footer>
);
}
src/components/footer/Footer/Footer.module.css
.footer {
background-color: var(--color-footer-bg, var(--color-surface));
border-top: 1px solid var(--color-border);
padding: 4rem 0 2rem;
}
.container {
max-width: var(--container-max-width, 1200px);
margin: 0 auto;
padding: 0 1.5rem;
}
/* Dynamic column grid using CSS custom property. */
.columns {
display: grid;
grid-template-columns: repeat(var(--column-count, 4), 1fr);
gap: 2rem;
margin-bottom: 3rem;
}
/* Footer bottom section. */
.bottom {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
}
.bottomLeft,
.bottomRight {
flex-shrink: 0;
}
.bottomCenter {
flex: 1;
text-align: center;
}
.copyright {
margin: 0;
font-size: 0.875rem;
color: var(--color-text-muted);
}
/* Responsive: stack columns on tablet. */
@media (max-width: 900px) {
.columns {
grid-template-columns: repeat(2, 1fr);
}
}
/* Responsive: stack columns on mobile. */
@media (max-width: 600px) {
.footer {
padding: 3rem 0 1.5rem;
}
.columns {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.bottom {
flex-direction: column;
text-align: center;
}
.bottomLeft,
.bottomCenter,
.bottomRight {
width: 100%;
}
}

FooterColumn Component

src/components/footer/Footer/FooterColumn.tsx
import { NewsletterForm } from '../NewsletterForm';
import styles from './FooterColumn.module.css';
interface FooterColumnData {
type: 'links' | 'text' | 'newsletter';
title: string;
links?: { label: string; url: string }[];
text?: string;
}
interface FooterColumnProps {
data: FooterColumnData;
}
/**
* FooterColumn renders different content types.
* - links: Navigation menu links
* - text: Rich text content (about us, store info)
* - newsletter: Email signup form
*/
export function FooterColumn({ data }: FooterColumnProps) {
const { type, title, links, text } = data;
return (
<div className={styles.column}>
{/* Column title */}
<h3 className={styles.title}>{title}</h3>
{/* Column content based on type */}
{type === 'links' && links && (
<nav aria-label={title}>
<ul className={styles.linkList}>
{links.map((link, index) => (
<li key={index}>
<a href={link.url} className={styles.link}>
{link.label}
</a>
</li>
))}
</ul>
</nav>
)}
{type === 'text' && text && (
<div
className={styles.text}
dangerouslySetInnerHTML={{ __html: text }}
/>
)}
{type === 'newsletter' && <NewsletterForm />}
</div>
);
}
src/components/footer/Footer/FooterColumn.module.css
.column {
min-width: 0; /* Prevent grid blowout. */
}
.title {
margin: 0 0 1rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text);
}
.linkList {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.link {
color: var(--color-text-muted);
text-decoration: none;
font-size: 0.9375rem;
transition: color 0.15s ease;
}
.link:hover {
color: var(--color-text);
text-decoration: underline;
}
.text {
font-size: 0.9375rem;
color: var(--color-text-muted);
line-height: 1.6;
}
.text p {
margin: 0 0 1rem;
}
.text p:last-child {
margin-bottom: 0;
}
src/components/footer/Footer/SocialLinks.tsx
import styles from './SocialLinks.module.css';
interface SocialLink {
platform: string;
url: string;
}
interface SocialLinksProps {
links: SocialLink[];
}
/**
* SocialLinks displays social media icons with links.
* Supports common platforms: facebook, instagram, twitter, youtube, pinterest, tiktok.
*/
export function SocialLinks({ links }: SocialLinksProps) {
return (
<nav className={styles.social} aria-label="Social media links">
<ul className={styles.list}>
{links.map((link) => (
<li key={link.platform}>
<a
href={link.url}
className={styles.link}
target="_blank"
rel="noopener noreferrer"
aria-label={`Visit us on ${link.platform}`}
>
<SocialIcon platform={link.platform} />
</a>
</li>
))}
</ul>
</nav>
);
}
/**
* SocialIcon returns the appropriate SVG icon for each platform.
*/
function SocialIcon({ platform }: { platform: string }) {
const icons: Record<string, JSX.Element> = {
facebook: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" />
</svg>
),
instagram: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5" />
</svg>
),
twitter: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
),
youtube: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z" />
</svg>
),
pinterest: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0a12 12 0 0 0-4.373 23.178c-.1-.937-.19-2.38.04-3.406l1.294-5.49s-.33-.66-.33-1.636c0-1.534.89-2.68 2-2.68.94 0 1.394.706 1.394 1.552 0 .946-.602 2.36-.912 3.67-.26 1.096.55 1.99 1.63 1.99 1.958 0 3.464-2.065 3.464-5.046 0-2.637-1.895-4.48-4.602-4.48-3.134 0-4.974 2.352-4.974 4.784 0 .947.365 1.962.82 2.514.09.11.103.206.076.318l-.306 1.25c-.048.203-.16.246-.37.148-1.379-.64-2.24-2.654-2.24-4.27 0-3.476 2.526-6.67 7.282-6.67 3.824 0 6.796 2.725 6.796 6.364 0 3.798-2.395 6.854-5.72 6.854-1.117 0-2.168-.58-2.527-1.266l-.688 2.624c-.25.962-.922 2.166-1.373 2.902A12 12 0 1 0 12 0z" />
</svg>
),
tiktok: (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z" />
</svg>
),
};
return icons[platform.toLowerCase()] || null;
}
src/components/footer/Footer/SocialLinks.module.css
.social {
display: inline-block;
}
.list {
display: flex;
gap: 0.75rem;
list-style: none;
margin: 0;
padding: 0;
}
.link {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
color: var(--color-text-muted);
background-color: transparent;
border-radius: var(--radius-full);
transition: color 0.15s ease, background-color 0.15s ease;
}
.link:hover {
color: var(--color-text);
background-color: var(--color-surface);
}
.link:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}

PaymentIcons Component

src/components/footer/Footer/PaymentIcons.tsx
import styles from './PaymentIcons.module.css';
interface PaymentIconsProps {
icons: string[];
}
/**
* PaymentIcons displays accepted payment method icons.
* Icons are rendered using Shopify's built-in payment icon system.
*/
export function PaymentIcons({ icons }: PaymentIconsProps) {
return (
<div className={styles.container} aria-label="Accepted payment methods">
<ul className={styles.list}>
{icons.map((icon) => (
<li key={icon} className={styles.item}>
<PaymentIcon type={icon} />
</li>
))}
</ul>
</div>
);
}
/**
* PaymentIcon renders individual payment method icons.
* Uses inline SVGs for common payment methods.
*/
function PaymentIcon({ type }: { type: string }) {
// Map of payment types to their SVG icons.
const paymentIcons: Record<string, JSX.Element> = {
visa: (
<svg viewBox="0 0 38 24" role="img" aria-label="Visa">
<rect fill="#fff" width="38" height="24" rx="3" />
<path fill="#142688" d="M15.6 8.3l-1.8 7.4h-2.2l1.8-7.4h2.2zm8.3 4.8l1.2-3.2.7 3.2h-1.9zm2.5 2.6h2l-1.7-7.4h-1.9c-.4 0-.8.2-1 .6l-3.4 6.8h2.4l.5-1.3h2.9l.2 1.3zm-6.2-2.4c0-2-.3-3.4-2.3-3.4-1 0-1.8.5-2.2 1.2l-.1-.1h-.1v-.9h-2.1v7.4h2.4v-4c0-1.2.6-1.9 1.6-1.9.9 0 1.3.6 1.3 1.8v4.1h2.4v-4.2h.1zM9.3 8.3l-3.4 7.4H3.5l-1.7-5.9c-.1-.4-.2-.5-.5-.7-.5-.3-1.3-.6-2-.8l.1-.1h3.8c.5 0 .9.3 1 .9l.9 5 2.3-5.8h2.4-.5z" />
</svg>
),
mastercard: (
<svg viewBox="0 0 38 24" role="img" aria-label="Mastercard">
<rect fill="#fff" width="38" height="24" rx="3" />
<circle fill="#EB001B" cx="15" cy="12" r="7" />
<circle fill="#F79E1B" cx="23" cy="12" r="7" />
<path fill="#FF5F00" d="M19 7a7 7 0 0 0-2 5 7 7 0 0 0 2 5 7 7 0 0 0 2-5 7 7 0 0 0-2-5z" />
</svg>
),
amex: (
<svg viewBox="0 0 38 24" role="img" aria-label="American Express">
<rect fill="#2557D6" width="38" height="24" rx="3" />
<path fill="#fff" d="M7 12.5h2.5L8.2 9.7l-1.2 2.8zm23.5-4h-3.2l-1.5 1.7-1.4-1.7h-9.8l-1 2.3-1-2.3H9.7L7 14.7h2.3l.5-1.2h2.9l.5 1.2h4.6v-3.5l1.8 3.5h1.6l1.8-3.5v3.5h2.3l1.5-1.8 1.5 1.8h2.2l-2.5-3 2.5-3.2z" />
</svg>
),
paypal: (
<svg viewBox="0 0 38 24" role="img" aria-label="PayPal">
<rect fill="#fff" width="38" height="24" rx="3" />
<path fill="#003087" d="M23.4 6.8c.4 2.3-.1 3.9-1.4 5.3-1.4 1.5-3.8 2.2-6.9 2.2h-.5c-.4 0-.8.3-.9.7l-.5 3-.3 1.9c0 .2.1.5.4.5h3c.4 0 .7-.3.8-.6v-.2l.5-3.1v-.2c.1-.4.4-.6.8-.6h.5c3.2 0 5.7-1.3 6.4-5 .3-1.6.2-2.9-.6-3.8-.3-.4-.6-.7-1-.9z" />
<path fill="#002F86" d="M22.2 6.4c-.1-.1-.3-.2-.5-.2-.2-.1-.3-.1-.5-.1-1.2-.2-2.5-.2-3.7-.2h-5.2c-.2 0-.3 0-.5.1-.3.2-.5.4-.5.8l-1.2 7.4v.2c.1-.4.5-.7.9-.7h1.9c3.6 0 6.4-1.5 7.2-5.7.1-.1.1-.3.1-.4.2-1 0-1.8-.7-2.2z" />
<path fill="#001C64" d="M12.4 6.8c0-.3.2-.6.5-.8.1-.1.3-.1.5-.1h5.2c.6 0 1.2 0 1.8.1h.7c.2 0 .4.1.5.1.1 0 .2.1.3.1.2.1.4.1.5.2.2-1.2 0-2-.7-2.8-.8-.9-2.2-1.3-4-1.3H10c-.4 0-.8.3-.9.7l-2.5 15.7c-.1.3.2.5.4.5h3.5l.9-5.6 1-6.8z" />
</svg>
),
shopify_pay: (
<svg viewBox="0 0 38 24" role="img" aria-label="Shop Pay">
<rect fill="#5A31F4" width="38" height="24" rx="3" />
<path fill="#fff" d="M21.4 9.3c-.4 0-.7.1-1 .4-.3.3-.4.6-.4 1s.1.7.4 1c.3.3.6.4 1 .4.4 0 .7-.1 1-.4.3-.3.4-.6.4-1s-.1-.7-.4-1c-.3-.3-.6-.4-1-.4zm.5 2.4c-.3.3-.6.4-.9.4-.4 0-.7-.1-.9-.4-.2-.3-.4-.5-.4-.9 0-.4.1-.7.4-.9.2-.3.5-.4.9-.4.3 0 .6.1.9.4.2.2.4.5.4.9 0 .4-.2.7-.4.9z" />
</svg>
),
apple_pay: (
<svg viewBox="0 0 38 24" role="img" aria-label="Apple Pay">
<rect fill="#000" width="38" height="24" rx="3" />
<path fill="#fff" d="M14.5 7.5c-.5.6-.9 1.4-.8 2.2.7.1 1.4-.4 1.9-1 .5-.6.8-1.4.7-2.1-.6 0-1.3.4-1.8.9zm.8 2.5c-1 0-1.8.6-2.3.6-.5 0-1.2-.5-2-.5-1 0-2 .6-2.5 1.5-1.1 1.9-.3 4.8.8 6.3.5.8 1.1 1.6 1.9 1.6.8 0 1.1-.5 2-.5s1.2.5 2 .5c.8 0 1.3-.8 1.8-1.5.6-.9.8-1.7.8-1.7s-1.6-.6-1.6-2.4c0-1.5 1.2-2.2 1.3-2.3-.7-1.1-1.9-1.2-2.3-1.2l.1-.4zM24.3 8v9.8h-1.5v-3.2H20v3.2h-1.5V8H20v4.1h2.7V8h1.6z" />
</svg>
),
google_pay: (
<svg viewBox="0 0 38 24" role="img" aria-label="Google Pay">
<rect fill="#fff" width="38" height="24" rx="3" />
<path fill="#4285F4" d="M19.1 12v2.4h3.5c-.1 1-.5 1.8-1 2.3-.6.6-1.6 1.3-3.1 1.3-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5c1.3 0 2.3.5 3 1.2l1.7-1.7c-1.1-1-2.6-1.8-4.7-1.8-3.9 0-7.1 3.2-7.1 7.1s3.2 7.1 7.1 7.1c2.1 0 3.6-.7 4.9-2 1.2-1.2 1.6-3 1.6-4.4 0-.4 0-.8-.1-1.1h-5.8v-.4z" />
</svg>
),
};
return (
<span className={styles.icon}>
{paymentIcons[type.toLowerCase()] || <span>{type}</span>}
</span>
);
}
src/components/footer/Footer/PaymentIcons.module.css
.container {
display: inline-block;
}
.list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
margin: 0;
padding: 0;
}
.item {
display: flex;
}
.icon {
display: block;
width: 38px;
height: 24px;
}
.icon svg {
width: 100%;
height: 100%;
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

Liquid Data Bridge

{% comment %} snippets/footer-data.liquid {% endcomment %}
{% comment %}
This snippet passes footer configuration to React.
Include in sections/footer.liquid or theme.liquid.
{% endcomment %}
<div id="footer-root"></div>
<script type="application/json" id="footer-data">
{
"columns": [
{%- for block in section.blocks -%}
{
"type": {{ block.type | json }},
"title": {{ block.settings.title | json }}
{%- if block.type == 'links' -%}
,"links": [
{%- assign menu = linklists[block.settings.menu] -%}
{%- for link in menu.links -%}
{
"label": {{ link.title | json }},
"url": {{ link.url | json }}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
{%- elsif block.type == 'text' -%}
,"text": {{ block.settings.text | json }}
{%- endif -%}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
],
"socialLinks": [
{%- if settings.social_facebook != blank -%}
{"platform": "facebook", "url": {{ settings.social_facebook | json }}}{% if settings.social_instagram != blank or settings.social_twitter != blank or settings.social_youtube != blank %},{% endif %}
{%- endif -%}
{%- if settings.social_instagram != blank -%}
{"platform": "instagram", "url": {{ settings.social_instagram | json }}}{% if settings.social_twitter != blank or settings.social_youtube != blank %},{% endif %}
{%- endif -%}
{%- if settings.social_twitter != blank -%}
{"platform": "twitter", "url": {{ settings.social_twitter | json }}}{% if settings.social_youtube != blank %},{% endif %}
{%- endif -%}
{%- if settings.social_youtube != blank -%}
{"platform": "youtube", "url": {{ settings.social_youtube | json }}}
{%- endif -%}
],
"paymentIcons": {{ shop.enabled_payment_types | json }},
"showPaymentIcons": {{ section.settings.show_payment_icons | json }},
"copyrightText": "© {{ 'now' | date: '%Y' }} {{ shop.name }}. All rights reserved."
}
</script>
src/entries/footer.tsx
import { createRoot } from 'react-dom/client';
import { Footer } from '@/components/footer/Footer';
/**
* Initialize and mount the footer component.
*/
export function mountFooter() {
const root = document.getElementById('footer-root');
const dataScript = document.getElementById('footer-data');
if (!root || !dataScript) return;
try {
const data = JSON.parse(dataScript.textContent || '{}');
createRoot(root).render(<Footer data={data} />);
} catch (error) {
console.error('Failed to mount footer:', error);
}
}

Key Takeaways

  1. Dynamic columns: Use CSS Grid with custom properties for flexible column layouts
  2. Component composition: Split footer into smaller, focused components (columns, social, payments)
  3. Multiple column types: Support links, text, and newsletter blocks
  4. Accessibility: Use proper ARIA labels and semantic HTML for navigation regions
  5. Responsive design: Stack columns on smaller screens with progressive grid changes
  6. Liquid integration: Pass all footer configuration as JSON for React to consume
  7. Payment icons: Display trust signals with accepted payment methods

In the next lesson, we’ll build the newsletter signup form with proper form handling and Shopify API integration.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...