Global UI: Footer Intermediate 12 min read

Dynamic Columns and Grid Patterns

Build flexible footer layouts using CSS Grid, blocks for dynamic columns, and responsive patterns that adapt to any content.

A well-designed footer adapts to different amounts of content while maintaining visual balance. Let’s build a flexible column system using CSS Grid and Shopify blocks.

CSS Grid provides the most flexible approach for footer layouts:

.footer__columns {
display: grid;
grid-template-columns: repeat(var(--columns, 4), 1fr);
gap: var(--spacing-xl);
}

Control columns from Liquid:

<div
class="footer__columns"
style="--columns: {{ section.settings.columns_desktop }};"
>
{%- for block in section.blocks -%}
<div class="footer__column">
<!-- column content -->
</div>
{%- endfor -%}
</div>

Responsive Column Behavior

Adapt columns at different breakpoints:

.footer__columns {
display: grid;
gap: var(--spacing-lg);
/* Mobile: Stack columns */
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.footer__columns {
/* Tablet: 2 columns */
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.footer__columns {
/* Desktop: Use setting */
grid-template-columns: repeat(var(--columns, 4), 1fr);
}
}

Auto-Fit Pattern

Let the browser decide column count:

.footer__columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
}

This creates as many 200px+ columns as will fit.

Block Types for Columns

Define different block types for various content:

{# snippets/footer-menu.liquid #}
{%- if block.settings.heading != blank -%}
<h3 class="footer__heading">{{ block.settings.heading }}</h3>
{%- endif -%}
{%- assign menu = linklists[block.settings.menu] -%}
{%- if menu.links.size > 0 -%}
<ul class="footer__menu">
{%- for link in menu.links -%}
<li>
<a href="{{ link.url }}" class="footer__link">
{{ link.title }}
</a>
</li>
{%- endfor -%}
</ul>
{%- endif -%}

Text Block

{# snippets/footer-text.liquid #}
{%- if block.settings.heading != blank -%}
<h3 class="footer__heading">{{ block.settings.heading }}</h3>
{%- endif -%}
{%- if block.settings.content != blank -%}
<div class="footer__text rte">
{{ block.settings.content }}
</div>
{%- endif -%}

Newsletter Block

{# snippets/footer-newsletter.liquid #}
{%- if block.settings.heading != blank -%}
<h3 class="footer__heading">{{ block.settings.heading }}</h3>
{%- endif -%}
{%- if block.settings.subheading != blank -%}
<p class="footer__subheading">{{ block.settings.subheading }}</p>
{%- endif -%}
{% form 'customer', class: 'footer__newsletter-form' %}
<div class="footer__newsletter-field">
<input
type="email"
name="contact[email]"
placeholder="{{ 'general.newsletter.email_placeholder' | t }}"
required
autocomplete="email"
class="footer__newsletter-input"
>
<button type="submit" class="footer__newsletter-button">
{{ block.settings.button_text | default: 'Subscribe' }}
</button>
</div>
{%- if form.posted_successfully? -%}
<p class="footer__newsletter-success">
{{ 'general.newsletter.confirmation' | t }}
</p>
{%- endif -%}
{% endform %}

Image Block

{# snippets/footer-image.liquid #}
{%- if block.settings.image -%}
{%- if block.settings.link != blank -%}
<a href="{{ block.settings.link }}" class="footer__image-link">
{%- endif -%}
<img
src="{{ block.settings.image | image_url: width: 400 }}"
alt="{{ block.settings.image.alt | default: '' }}"
width="{{ block.settings.image.width }}"
height="{{ block.settings.image.height }}"
loading="lazy"
class="footer__image"
>
{%- if block.settings.link != blank -%}
</a>
{%- endif -%}
{%- endif -%}

Column Width Settings

Let merchants control individual column widths:

{
"type": "menu",
"name": "Menu Column",
"settings": [
{
"type": "select",
"id": "column_width",
"label": "Column width",
"options": [
{ "value": "small", "label": "Small" },
{ "value": "medium", "label": "Medium" },
{ "value": "large", "label": "Large" }
],
"default": "medium"
}
]
}
<div
class="footer__column footer__column--{{ block.settings.column_width }}"
{{ block.shopify_attributes }}
>
.footer__columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--spacing-lg);
}
@media (min-width: 1024px) {
.footer__column--small {
grid-column: span 1;
}
.footer__column--medium {
grid-column: span 1;
}
.footer__column--large {
grid-column: span 2;
}
}

Handling Variable Content Heights

When columns have different amounts of content, align them properly:

/* Align content to top */
.footer__column {
display: flex;
flex-direction: column;
align-items: flex-start;
}
/* Push last element to bottom (optional) */
.footer__column > *:last-child {
margin-top: auto;
}

Equal Height Columns

Grid naturally creates equal-height columns. For internal alignment:

.footer__columns {
display: grid;
grid-template-columns: repeat(4, 1fr);
align-items: start; /* Top-align columns */
}
/* Or stretch to fill */
.footer__columns {
align-items: stretch;
}
{# sections/footer.liquid #}
{%- liquid
assign bg_color = section.settings.background_color
assign text_color = section.settings.text_color
assign columns = section.settings.columns_desktop
-%}
<footer
class="footer"
style="
--footer-bg: {{ bg_color }};
--footer-text: {{ text_color }};
--footer-columns: {{ columns }};
"
>
<div class="footer__container container">
{# Main columns area #}
{%- if section.blocks.size > 0 -%}
<div class="footer__main">
<div class="footer__columns">
{%- for block in section.blocks -%}
<div
class="footer__column footer__column--{{ block.settings.column_width | default: 'medium' }}"
{{ block.shopify_attributes }}
>
{%- case block.type -%}
{%- when 'menu' -%}
{%- render 'footer-menu', block: block -%}
{%- when 'text' -%}
{%- render 'footer-text', block: block -%}
{%- when 'newsletter' -%}
{%- render 'footer-newsletter', block: block -%}
{%- when 'image' -%}
{%- render 'footer-image', block: block -%}
{%- when 'social' -%}
{%- render 'footer-social', block: block -%}
{%- endcase -%}
</div>
{%- endfor -%}
</div>
</div>
{%- endif -%}
{# Bottom bar #}
<div class="footer__bottom">
<div class="footer__bottom-content">
{# Copyright #}
<p class="footer__copyright">
&copy; {{ 'now' | date: '%Y' }} {{ shop.name }}
</p>
{# Policy links #}
{%- if section.settings.show_policy_links -%}
<nav class="footer__policies" aria-label="Policy links">
{%- for policy in shop.policies -%}
{%- if policy != blank -%}
<a href="{{ policy.url }}">{{ policy.title }}</a>
{%- endif -%}
{%- endfor -%}
</nav>
{%- endif -%}
{# Payment icons #}
{%- if section.settings.show_payment_icons -%}
<div class="footer__payment">
{%- render 'payment-icons' -%}
</div>
{%- endif -%}
</div>
</div>
</div>
</footer>
{% schema %}
{
"name": "Footer",
"class": "section-footer",
"settings": [
{
"type": "header",
"content": "Layout"
},
{
"type": "range",
"id": "columns_desktop",
"label": "Desktop columns",
"min": 2,
"max": 5,
"step": 1,
"default": 4
},
{
"type": "header",
"content": "Colors"
},
{
"type": "color",
"id": "background_color",
"label": "Background",
"default": "#1a1a1a"
},
{
"type": "color",
"id": "text_color",
"label": "Text",
"default": "#ffffff"
},
{
"type": "header",
"content": "Bottom bar"
},
{
"type": "checkbox",
"id": "show_policy_links",
"label": "Show policy links",
"default": true
},
{
"type": "checkbox",
"id": "show_payment_icons",
"label": "Show payment icons",
"default": true
}
],
"blocks": [
{
"type": "menu",
"name": "Menu",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Quick Links"
},
{
"type": "link_list",
"id": "menu",
"label": "Menu",
"default": "footer"
}
]
},
{
"type": "text",
"name": "Text",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading"
},
{
"type": "richtext",
"id": "content",
"label": "Content"
}
]
},
{
"type": "newsletter",
"name": "Newsletter",
"limit": 1,
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Subscribe"
},
{
"type": "text",
"id": "subheading",
"label": "Description"
},
{
"type": "text",
"id": "button_text",
"label": "Button text",
"default": "Subscribe"
}
]
},
{
"type": "image",
"name": "Image",
"settings": [
{
"type": "image_picker",
"id": "image",
"label": "Image"
},
{
"type": "url",
"id": "link",
"label": "Link"
}
]
}
],
"presets": [
{
"name": "Footer",
"blocks": [
{ "type": "menu" },
{ "type": "menu" },
{ "type": "text" },
{ "type": "newsletter" }
]
}
]
}
{% endschema %}
.footer {
background: var(--footer-bg, #1a1a1a);
color: var(--footer-text, #ffffff);
padding: var(--spacing-2xl) 0 var(--spacing-lg);
}
.footer__main {
padding-bottom: var(--spacing-xl);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
/* Columns grid */
.footer__columns {
display: grid;
gap: var(--spacing-lg);
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.footer__columns {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.footer__columns {
grid-template-columns: repeat(var(--footer-columns, 4), 1fr);
}
}
/* Column content */
.footer__column {
display: flex;
flex-direction: column;
}
.footer__heading {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--spacing-md);
}
/* Menu styles */
.footer__menu {
list-style: none;
padding: 0;
margin: 0;
}
.footer__menu li {
margin-bottom: var(--spacing-xs);
}
.footer__link {
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: color 0.2s;
}
.footer__link:hover {
color: #ffffff;
}
/* Text content */
.footer__text {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9375rem;
line-height: 1.6;
}
/* Newsletter */
.footer__subheading {
color: rgba(255, 255, 255, 0.7);
margin-bottom: var(--spacing-md);
}
.footer__newsletter-field {
display: flex;
gap: var(--spacing-xs);
}
.footer__newsletter-input {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid rgba(255, 255, 255, 0.2);
background: transparent;
color: inherit;
}
.footer__newsletter-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.footer__newsletter-button {
padding: var(--spacing-sm) var(--spacing-md);
background: #ffffff;
color: #000000;
border: none;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.footer__newsletter-button:hover {
opacity: 0.9;
}
/* Bottom bar */
.footer__bottom {
padding-top: var(--spacing-lg);
}
.footer__bottom-content {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md);
}
.footer__copyright {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.5);
margin: 0;
}
.footer__policies {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-md);
}
.footer__policies a {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
}
.footer__policies a:hover {
color: #ffffff;
}
.footer__payment {
display: flex;
gap: var(--spacing-xs);
}

Practice Exercise

Build a footer that:

  1. Has 4 blocks (2 menus, 1 text, 1 newsletter)
  2. Displays 4 columns on desktop, 2 on tablet, 1 on mobile
  3. Shows payment icons in the bottom bar
  4. Has proper hover states on links

Test by adding and removing blocks in the theme editor.

Key Takeaways

  1. CSS Grid provides the most flexible column layouts
  2. Use blocks for merchant-configurable columns
  3. Provide multiple block types: menu, text, newsletter, image
  4. Set responsive breakpoints for column stacking
  5. Use CSS custom properties for dynamic column counts
  6. Handle varying content heights with flexbox alignment
  7. Include sensible presets for quick setup

What’s Next?

With the column structure in place, the next lesson covers Social Links and Policy Links for adding the standard footer elements.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...