Footer and Content Components Intermediate 12 min read

Reusable Content Section Components

Build flexible content section components with React for rich text, image layouts, and feature grids. Learn patterns for composable sections that integrate with Shopify theme editor.

Content sections are the building blocks of marketing pages—they display rich text, images, videos, and call-to-actions. Let’s build a library of reusable content components that work seamlessly with Shopify’s theme editor.

Theme Integration

Each content section type has its own Liquid section file that passes data to React:

sections/rich-text.liquid
└── <div id="rich-text-{{ section.id }}">
└── RichTextSection (React) ← You are here
sections/image-with-text.liquid
└── <div id="image-text-{{ section.id }}">
└── ImageWithText (React) ← You are here
sections/feature-grid.liquid
└── <div id="feature-grid-{{ section.id }}">
└── FeatureGrid (React) ← You are here

Each section passes its block data via JSON script tags. The section.id ensures unique mount points when sections are used multiple times.

Data Source

Prop/StateSourceLiquid Field
RichTextSection
headingJSON script elementsection.settings.heading
headingSizeJSON script elementsection.settings.heading_size
contentJSON script elementsection.settings.text (richtext)
alignmentJSON script elementsection.settings.alignment
buttonJSON script elementsection.settings.button_text, button_link, button_style
backgroundJSON script elementsection.settings.background
ImageWithText
image.srcJSON script elementsection.settings.image | image_url
image.altJSON script elementsection.settings.image.alt
headingJSON script elementsection.settings.heading
contentJSON script elementsection.settings.text
imagePositionJSON script elementsection.settings.image_position
FeatureGrid
features[]JSON script elementsection.blocks (type: feature)
features[].iconJSON script elementblock.settings.icon
features[].headingJSON script elementblock.settings.heading
features[].textJSON script elementblock.settings.text
columnsJSON script elementsection.settings.columns

Note: All content section props are serialized from Liquid section settings and blocks. These sections are configured via the Shopify theme editor.

Content Section Patterns

┌────────────────────────────────────────────────────────────────────────────────┐
│ CONTENT SECTION TYPES │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ RICH TEXT SECTION │ │
│ │ ┌───────────────────────────────────────────────────────────────────┐ │ │
│ │ │ Welcome to Our Store │ │ │
│ │ │ We're dedicated to bringing you quality products that make │ │ │
│ │ │ a difference. Founded in 2020, we've grown from a small... │ │ │
│ │ │ [Learn More] │ │ │
│ │ └───────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ IMAGE WITH TEXT │ │
│ │ ┌─────────────────────────┐ ┌───────────────────────────────────────┐ │ │
│ │ │ │ │ Our Story │ │ │
│ │ │ [IMAGE] │ │ From humble beginnings to a │ │ │
│ │ │ │ │ global brand, we've stayed true... │ │ │
│ │ │ │ │ [Shop Collection] │ │ │
│ │ └─────────────────────────┘ └───────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ FEATURE GRID │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ [icon] │ │ [icon] │ │ [icon] │ │ │
│ │ │ Free Shipping│ │ Easy Returns │ │ 24/7 Support │ │ │
│ │ │ On orders $50+│ │ 30-day policy│ │ We're here │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────┘

Section Wrapper Component

src/components/content/SectionWrapper/SectionWrapper.tsx
import { ReactNode } from 'react';
import styles from './SectionWrapper.module.css';
interface SectionWrapperProps {
children: ReactNode;
id?: string;
background?: 'none' | 'primary' | 'secondary' | 'accent';
paddingTop?: 'none' | 'small' | 'medium' | 'large';
paddingBottom?: 'none' | 'small' | 'medium' | 'large';
width?: 'narrow' | 'normal' | 'wide' | 'full';
className?: string;
}
/**
* SectionWrapper provides consistent spacing and background
* options for all content sections.
*/
export function SectionWrapper({
children,
id,
background = 'none',
paddingTop = 'medium',
paddingBottom = 'medium',
width = 'normal',
className = '',
}: SectionWrapperProps) {
const classes = [
styles.section,
styles[`bg-${background}`],
styles[`pt-${paddingTop}`],
styles[`pb-${paddingBottom}`],
styles[`width-${width}`],
className,
]
.filter(Boolean)
.join(' ');
return (
<section id={id} className={classes}>
<div className={styles.container}>{children}</div>
</section>
);
}
src/components/content/SectionWrapper/SectionWrapper.module.css
.section {
width: 100%;
}
/* Background variants */
.bg-none {
background-color: transparent;
}
.bg-primary {
background-color: var(--color-background);
}
.bg-secondary {
background-color: var(--color-surface);
}
.bg-accent {
background-color: var(--color-accent-bg, var(--color-primary));
color: var(--color-accent-text, #fff);
}
/* Padding top */
.pt-none {
padding-top: 0;
}
.pt-small {
padding-top: 2rem;
}
.pt-medium {
padding-top: 4rem;
}
.pt-large {
padding-top: 6rem;
}
/* Padding bottom */
.pb-none {
padding-bottom: 0;
}
.pb-small {
padding-bottom: 2rem;
}
.pb-medium {
padding-bottom: 4rem;
}
.pb-large {
padding-bottom: 6rem;
}
/* Container widths */
.container {
margin: 0 auto;
padding: 0 1.5rem;
}
.width-narrow .container {
max-width: 680px;
}
.width-normal .container {
max-width: 1000px;
}
.width-wide .container {
max-width: 1200px;
}
.width-full .container {
max-width: none;
padding: 0;
}
/* Responsive padding */
@media (max-width: 768px) {
.pt-medium {
padding-top: 3rem;
}
.pt-large {
padding-top: 4rem;
}
.pb-medium {
padding-bottom: 3rem;
}
.pb-large {
padding-bottom: 4rem;
}
}

RichText Section

src/components/content/RichTextSection/RichTextSection.tsx
import { SectionWrapper } from '../SectionWrapper';
import { Button } from '@/components/ui';
import styles from './RichTextSection.module.css';
interface RichTextSectionProps {
id?: string;
heading?: string;
headingSize?: 'small' | 'medium' | 'large';
content: string;
alignment?: 'left' | 'center' | 'right';
button?: {
text: string;
url: string;
style?: 'primary' | 'secondary' | 'outline';
};
background?: 'none' | 'primary' | 'secondary' | 'accent';
}
/**
* RichTextSection displays formatted text content with
* optional heading and call-to-action button.
*/
export function RichTextSection({
id,
heading,
headingSize = 'medium',
content,
alignment = 'center',
button,
background = 'none',
}: RichTextSectionProps) {
return (
<SectionWrapper id={id} background={background} width="narrow">
<div className={`${styles.content} ${styles[`align-${alignment}`]}`}>
{/* Heading */}
{heading && (
<h2 className={`${styles.heading} ${styles[`heading-${headingSize}`]}`}>
{heading}
</h2>
)}
{/* Rich text content */}
<div
className={styles.text}
dangerouslySetInnerHTML={{ __html: content }}
/>
{/* Call-to-action button */}
{button && (
<div className={styles.buttonWrapper}>
<Button
as="a"
href={button.url}
variant={button.style || 'primary'}
>
{button.text}
</Button>
</div>
)}
</div>
</SectionWrapper>
);
}
src/components/content/RichTextSection/RichTextSection.module.css
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Text alignment */
.align-left {
text-align: left;
align-items: flex-start;
}
.align-center {
text-align: center;
align-items: center;
}
.align-right {
text-align: right;
align-items: flex-end;
}
/* Heading sizes */
.heading {
margin: 0;
font-weight: 600;
line-height: 1.2;
}
.heading-small {
font-size: 1.5rem;
}
.heading-medium {
font-size: 2rem;
}
.heading-large {
font-size: 2.5rem;
}
/* Rich text styling */
.text {
font-size: 1.125rem;
line-height: 1.7;
color: var(--color-text-muted);
}
.text p {
margin: 0 0 1rem;
}
.text p:last-child {
margin-bottom: 0;
}
.text strong {
color: var(--color-text);
font-weight: 600;
}
.text a {
color: var(--color-primary);
text-decoration: underline;
}
.text a:hover {
text-decoration: none;
}
/* Button wrapper */
.buttonWrapper {
margin-top: 0.5rem;
}
/* Responsive */
@media (max-width: 768px) {
.heading-medium {
font-size: 1.75rem;
}
.heading-large {
font-size: 2rem;
}
.text {
font-size: 1rem;
}
}

ImageWithText Section

src/components/content/ImageWithText/ImageWithText.tsx
import { SectionWrapper } from '../SectionWrapper';
import { Button } from '@/components/ui';
import { OptimizedImage } from '@/components/ui/OptimizedImage';
import styles from './ImageWithText.module.css';
interface ImageWithTextProps {
id?: string;
image: {
src: string;
alt: string;
width?: number;
height?: number;
};
heading?: string;
content: string;
button?: {
text: string;
url: string;
style?: 'primary' | 'secondary' | 'outline';
};
imagePosition?: 'left' | 'right';
imageSize?: 'small' | 'medium' | 'large';
verticalAlign?: 'top' | 'center' | 'bottom';
background?: 'none' | 'primary' | 'secondary' | 'accent';
}
/**
* ImageWithText displays an image alongside text content.
* Supports flexible positioning and sizing options.
*/
export function ImageWithText({
id,
image,
heading,
content,
button,
imagePosition = 'left',
imageSize = 'medium',
verticalAlign = 'center',
background = 'none',
}: ImageWithTextProps) {
const layoutClass = imagePosition === 'right' ? styles.reversed : '';
const alignClass = styles[`align-${verticalAlign}`];
const sizeClass = styles[`image-${imageSize}`];
return (
<SectionWrapper id={id} background={background} width="wide">
<div className={`${styles.grid} ${layoutClass} ${alignClass} ${sizeClass}`}>
{/* Image column */}
<div className={styles.imageColumn}>
<div className={styles.imageWrapper}>
<OptimizedImage
src={image.src}
alt={image.alt}
width={image.width || 800}
height={image.height || 600}
className={styles.image}
/>
</div>
</div>
{/* Text column */}
<div className={styles.textColumn}>
{heading && <h2 className={styles.heading}>{heading}</h2>}
<div
className={styles.content}
dangerouslySetInnerHTML={{ __html: content }}
/>
{button && (
<div className={styles.buttonWrapper}>
<Button
as="a"
href={button.url}
variant={button.style || 'primary'}
>
{button.text}
</Button>
</div>
)}
</div>
</div>
</SectionWrapper>
);
}
src/components/content/ImageWithText/ImageWithText.module.css
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
}
/* Reverse layout for image on right */
.reversed {
direction: rtl;
}
.reversed > * {
direction: ltr;
}
/* Vertical alignment */
.align-top {
align-items: flex-start;
}
.align-center {
align-items: center;
}
.align-bottom {
align-items: flex-end;
}
/* Image sizes */
.image-small {
grid-template-columns: 1fr 2fr;
}
.image-small.reversed {
grid-template-columns: 2fr 1fr;
}
.image-medium {
grid-template-columns: 1fr 1fr;
}
.image-large {
grid-template-columns: 3fr 2fr;
}
.image-large.reversed {
grid-template-columns: 2fr 3fr;
}
/* Image column */
.imageColumn {
overflow: hidden;
}
.imageWrapper {
position: relative;
border-radius: var(--radius-lg, 8px);
overflow: hidden;
}
.image {
width: 100%;
height: auto;
display: block;
object-fit: cover;
}
/* Text column */
.textColumn {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1.25rem;
}
.heading {
margin: 0;
font-size: 2rem;
font-weight: 600;
line-height: 1.2;
}
.content {
font-size: 1.0625rem;
line-height: 1.7;
color: var(--color-text-muted);
}
.content p {
margin: 0 0 1rem;
}
.content p:last-child {
margin-bottom: 0;
}
.buttonWrapper {
margin-top: 0.5rem;
}
/* Responsive: stack on tablet */
@media (max-width: 900px) {
.grid,
.image-small,
.image-medium,
.image-large,
.image-small.reversed,
.image-large.reversed {
grid-template-columns: 1fr;
gap: 2rem;
}
.reversed {
direction: ltr;
}
.heading {
font-size: 1.75rem;
}
}

FeatureGrid Section

src/components/content/FeatureGrid/FeatureGrid.tsx
import { SectionWrapper } from '../SectionWrapper';
import styles from './FeatureGrid.module.css';
interface Feature {
icon: string;
heading: string;
text: string;
}
interface FeatureGridProps {
id?: string;
heading?: string;
features: Feature[];
columns?: 2 | 3 | 4;
background?: 'none' | 'primary' | 'secondary' | 'accent';
}
/**
* FeatureGrid displays a grid of features/benefits with icons.
* Great for trust signals, USPs, or service highlights.
*/
export function FeatureGrid({
id,
heading,
features,
columns = 3,
background = 'none',
}: FeatureGridProps) {
return (
<SectionWrapper id={id} background={background} width="wide">
{/* Section heading */}
{heading && <h2 className={styles.sectionHeading}>{heading}</h2>}
{/* Features grid */}
<div
className={styles.grid}
style={{ '--columns': columns } as React.CSSProperties}
>
{features.map((feature, index) => (
<div key={index} className={styles.feature}>
{/* Icon */}
<div className={styles.iconWrapper}>
<FeatureIcon name={feature.icon} />
</div>
{/* Text content */}
<h3 className={styles.heading}>{feature.heading}</h3>
<p className={styles.text}>{feature.text}</p>
</div>
))}
</div>
</SectionWrapper>
);
}
/**
* FeatureIcon renders common feature icons.
*/
function FeatureIcon({ name }: { name: string }) {
const icons: Record<string, JSX.Element> = {
shipping: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<rect x="1" y="3" width="15" height="13" />
<polygon points="16 8 20 8 23 11 23 16 16 16 16 8" />
<circle cx="5.5" cy="18.5" r="2.5" />
<circle cx="18.5" cy="18.5" r="2.5" />
</svg>
),
returns: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<polyline points="1 4 1 10 7 10" />
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
</svg>
),
support: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
),
secure: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
<path d="m9 12 2 2 4-4" />
</svg>
),
quality: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="12" cy="8" r="7" />
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88" />
</svg>
),
gift: (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<polyline points="20 12 20 22 4 22 4 12" />
<rect x="2" y="7" width="20" height="5" />
<line x1="12" y1="22" x2="12" y2="7" />
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z" />
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z" />
</svg>
),
};
return icons[name] || icons['quality'];
}
src/components/content/FeatureGrid/FeatureGrid.module.css
.sectionHeading {
margin: 0 0 2.5rem;
font-size: 2rem;
font-weight: 600;
text-align: center;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--columns, 3), 1fr);
gap: 2rem;
}
.feature {
text-align: center;
padding: 1.5rem;
}
.iconWrapper {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
margin-bottom: 1rem;
border-radius: var(--radius-full);
background-color: var(--color-surface);
color: var(--color-primary);
}
.heading {
margin: 0 0 0.5rem;
font-size: 1.125rem;
font-weight: 600;
}
.text {
margin: 0;
font-size: 0.9375rem;
color: var(--color-text-muted);
line-height: 1.5;
}
/* Responsive */
@media (max-width: 900px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.grid {
grid-template-columns: 1fr;
}
.feature {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
}

Scroll Reveal Animation

src/hooks/useScrollReveal.ts
import { useEffect, useRef, useState } from 'react';
interface UseScrollRevealOptions {
threshold?: number;
rootMargin?: string;
triggerOnce?: boolean;
}
/**
* Hook for triggering animations when elements enter the viewport.
*/
export function useScrollReveal<T extends HTMLElement>({
threshold = 0.1,
rootMargin = '0px',
triggerOnce = true,
}: UseScrollRevealOptions = {}) {
const ref = useRef<T>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
if (triggerOnce) {
observer.unobserve(element);
}
} else if (!triggerOnce) {
setIsVisible(false);
}
},
{ threshold, rootMargin }
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold, rootMargin, triggerOnce]);
return { ref, isVisible };
}
src/components/content/AnimatedSection/AnimatedSection.tsx
import { ReactNode } from 'react';
import { useScrollReveal } from '@/hooks/useScrollReveal';
import styles from './AnimatedSection.module.css';
interface AnimatedSectionProps {
children: ReactNode;
animation?: 'fadeIn' | 'fadeUp' | 'fadeLeft' | 'fadeRight' | 'scale';
delay?: number;
className?: string;
}
/**
* AnimatedSection wraps content with scroll-triggered animations.
*/
export function AnimatedSection({
children,
animation = 'fadeUp',
delay = 0,
className = '',
}: AnimatedSectionProps) {
const { ref, isVisible } = useScrollReveal<HTMLDivElement>();
const animationClass = isVisible ? styles[animation] : styles.hidden;
return (
<div
ref={ref}
className={`${styles.wrapper} ${animationClass} ${className}`}
style={{ transitionDelay: `${delay}ms` }}
>
{children}
</div>
);
}
src/components/content/AnimatedSection/AnimatedSection.module.css
.wrapper {
transition: opacity 0.6s ease, transform 0.6s ease;
}
/* Hidden state */
.hidden {
opacity: 0;
}
/* Animation variants */
.fadeIn {
opacity: 1;
}
.fadeUp {
opacity: 1;
transform: translateY(0);
}
.hidden.fadeUp {
transform: translateY(30px);
}
.fadeLeft {
opacity: 1;
transform: translateX(0);
}
.hidden.fadeLeft {
transform: translateX(-30px);
}
.fadeRight {
opacity: 1;
transform: translateX(0);
}
.hidden.fadeRight {
transform: translateX(30px);
}
.scale {
opacity: 1;
transform: scale(1);
}
.hidden.scale {
transform: scale(0.95);
}
/* Reduce motion for accessibility */
@media (prefers-reduced-motion: reduce) {
.wrapper {
transition: none;
}
.hidden {
opacity: 1;
transform: none;
}
}

Liquid Section Integration

{% comment %} sections/rich-text.liquid {% endcomment %}
<div
id="rich-text-{{ section.id }}"
data-component="rich-text-section"
></div>
<script type="application/json" id="rich-text-data-{{ section.id }}">
{
"id": {{ section.id | json }},
"heading": {{ section.settings.heading | json }},
"headingSize": {{ section.settings.heading_size | json }},
"content": {{ section.settings.text | json }},
"alignment": {{ section.settings.alignment | json }},
"background": {{ section.settings.background | json }},
{%- if section.settings.button_text != blank -%}
"button": {
"text": {{ section.settings.button_text | json }},
"url": {{ section.settings.button_link | json }},
"style": {{ section.settings.button_style | json }}
}
{%- else -%}
"button": null
{%- endif -%}
}
</script>
{% schema %}
{
"name": "Rich text",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading"
},
{
"type": "select",
"id": "heading_size",
"label": "Heading size",
"options": [
{ "value": "small", "label": "Small" },
{ "value": "medium", "label": "Medium" },
{ "value": "large", "label": "Large" }
],
"default": "medium"
},
{
"type": "richtext",
"id": "text",
"label": "Text",
"default": "<p>Share your brand story with customers.</p>"
},
{
"type": "select",
"id": "alignment",
"label": "Text alignment",
"options": [
{ "value": "left", "label": "Left" },
{ "value": "center", "label": "Center" },
{ "value": "right", "label": "Right" }
],
"default": "center"
},
{
"type": "select",
"id": "background",
"label": "Background",
"options": [
{ "value": "none", "label": "None" },
{ "value": "primary", "label": "Primary" },
{ "value": "secondary", "label": "Secondary" }
],
"default": "none"
},
{
"type": "text",
"id": "button_text",
"label": "Button text"
},
{
"type": "url",
"id": "button_link",
"label": "Button link"
},
{
"type": "select",
"id": "button_style",
"label": "Button style",
"options": [
{ "value": "primary", "label": "Primary" },
{ "value": "secondary", "label": "Secondary" },
{ "value": "outline", "label": "Outline" }
],
"default": "primary"
}
],
"presets": [
{
"name": "Rich text"
}
]
}
{% endschema %}

Mounting Content Sections

src/entries/content-sections.tsx
import { createRoot } from 'react-dom/client';
import { RichTextSection } from '@/components/content/RichTextSection';
import { ImageWithText } from '@/components/content/ImageWithText';
import { FeatureGrid } from '@/components/content/FeatureGrid';
type SectionComponent = typeof RichTextSection | typeof ImageWithText | typeof FeatureGrid;
const COMPONENTS: Record<string, SectionComponent> = {
'rich-text-section': RichTextSection,
'image-with-text': ImageWithText,
'feature-grid': FeatureGrid,
};
/**
* Mount all content section components on the page.
*/
export function mountContentSections() {
// Find all section mount points.
const sections = document.querySelectorAll('[data-component]');
sections.forEach((element) => {
const componentName = element.getAttribute('data-component');
if (!componentName || !COMPONENTS[componentName]) return;
// Find corresponding data script.
const sectionId = element.id.split('-').pop();
const dataScript = document.getElementById(`${componentName.replace('-section', '')}-data-${sectionId}`);
if (!dataScript) return;
try {
const data = JSON.parse(dataScript.textContent || '{}');
const Component = COMPONENTS[componentName];
createRoot(element).render(<Component {...data} />);
} catch (error) {
console.error(`Failed to mount ${componentName}:`, error);
}
});
}

Key Takeaways

  1. Composable sections: Use SectionWrapper for consistent spacing and backgrounds
  2. Flexible layouts: Support multiple alignment, sizing, and positioning options
  3. Rich content types: Handle text, images, and structured content
  4. Scroll animations: Add reveal effects with IntersectionObserver
  5. Accessibility: Respect prefers-reduced-motion for animations
  6. Liquid integration: Pass section settings as JSON for React consumption
  7. Reusable patterns: Build a library of sections that work together

You’ve now completed Module 11 and have a solid foundation of footer and content components. In the next module, we’ll explore customer account components for login, registration, and order management.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...