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 for Footer Columns
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:
Menu Block
{# 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;}Complete Footer Section
{# 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"> © {{ '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 CSS
.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:
- Has 4 blocks (2 menus, 1 text, 1 newsletter)
- Displays 4 columns on desktop, 2 on tablet, 1 on mobile
- Shows payment icons in the bottom bar
- Has proper hover states on links
Test by adding and removing blocks in the theme editor.
Key Takeaways
- CSS Grid provides the most flexible column layouts
- Use blocks for merchant-configurable columns
- Provide multiple block types: menu, text, newsletter, image
- Set responsive breakpoints for column stacking
- Use CSS custom properties for dynamic column counts
- Handle varying content heights with flexbox alignment
- 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...