Talk Shop
Home
Learn More
About Us
Follow Us
Blog
Tools
Newsletter
Join Discord
Join

Community

  • Developers
  • Growth
  • Entrepreneurs
  • Support
  • Experts
  • Tools

Location

123 Mars, Crater City, Red Planet

(WiFi may be spotty)

Hours

Who has time for breaks? We're here 24/7!

Contact

hello@letstalkshop.com

Talk Shop
Talk Shop

Built for real builders. Not affiliated with Shopify Inc.

Home
Privacy
Terms
  1. Home
  2. >Blog
  3. >Shopify Development
  4. >How to Build Hero Sections in Shopify: Code Examples and Design Patterns
Shopify Development30 min read

How to Build Hero Sections in Shopify: Code Examples and Design Patterns

Build high-converting Shopify hero sections from scratch with Liquid code examples. Covers full-width banners, video heroes, split layouts, slideshow alternatives, and mobile optimization.

Talk Shop

Talk Shop

Mar 16, 2026

How to Build Hero Sections in Shopify: Code Examples and Design Patterns

In this article

  • Your Hero Section Is the Most Important 600 Pixels on Your Store
  • Anatomy of a Shopify Hero Section
  • Full-Width Image Hero With Schema Settings
  • Video Background Hero Section
  • Split-Layout Hero: Image and Text Side by Side
  • Overlay and Gradient Techniques That Actually Work
  • Responsive Design and Mobile-First Hero Optimization
  • Preloading the LCP Image for Performance
  • Hero Section With Countdown Timer
  • A/B Testing Hero Variants With Liquid
  • Choosing the Right Hero Pattern for Your Store
  • Putting It All Together: Your Hero Section Checklist

Your Hero Section Is the Most Important 600 Pixels on Your Store

The hero section is the first thing every visitor sees. It loads before they scroll, before they read your product descriptions, before they consider your pricing. On average, visitors form an opinion about your store within 50 milliseconds — and the hero section owns that impression. According to data from stores we've worked with at Talk Shop, hero sections account for over 40% of homepage click-throughs when built correctly.

Yet most Shopify stores rely on whatever hero section ships with their theme. The default is serviceable but generic. If you want a hero that actually converts — one that matches your brand, loads fast, and adapts to any device — you need to build it yourself.

This tutorial walks through building five distinct hero section patterns in Liquid, each with complete code you can paste into your theme today. We cover the anatomy of a hero section, full-width image banners, video backgrounds, split layouts, overlay techniques, mobile optimization, performance tuning for LCP, countdown timers, and A/B testing patterns. Whether you're a developer building for clients or a merchant customizing your own store, every pattern here is production-ready.

If you're new to Shopify theme development, start with our beginner's guide to Shopify development before diving into hero-specific code. For a broader look at building sections in general, our custom Shopify sections tutorial covers the foundational concepts this article builds on.

Anatomy of a Shopify Hero Section

Holographic UI panel visualizing a full-width image wireframe with data input nodes against a dark void.

Before writing any code, understand the three layers that make up every hero section file in Online Store 2.0.

Layer 1: Liquid Markup

The HTML structure rendered to the browser. This includes your image or video container, text overlay, and call-to-action elements. Liquid template tags pull in dynamic values from the theme editor.

Layer 2: CSS Styles

Either inline within a {% style %} tag or loaded from a separate file in your assets/ directory. The CSS controls layout, positioning, responsiveness, and visual effects like overlays and gradients.

Layer 3: Schema Settings

The {% schema %} block at the bottom of the file. This JSON object defines what controls appear in the Shopify theme editor — image pickers, text inputs, color selectors, range sliders. It's what makes your section merchant-editable without code changes.

Here's how the three layers relate:

LayerPurposeFile LocationWho Interacts
Liquid MarkupRenders HTML with dynamic valuessections/hero-*.liquidDeveloper writes, browser renders
CSS StylesVisual presentation and layoutassets/section-hero-*.cssDeveloper writes, browser applies
Schema SettingsTheme editor controlsBottom of section fileDeveloper defines, merchant configures

Every hero pattern in this tutorial follows this three-layer structure. The schema is what separates a hardcoded HTML block from a proper Shopify section — it gives merchants full control through the visual editor.

For a deeper understanding of how sections, blocks, and schema interact, Shopify's official section schema documentation is the definitive reference.

Full-Width Image Hero With Schema Settings

This is the most common hero pattern: a full-bleed background image with centered text and a CTA button. It's what most visitors expect on a homepage. Here's a production-ready version with responsive images, overlay control, and flexible alignment.

The Liquid Markup

Create sections/hero-image.liquid:

liquidliquid
{{ 'section-hero-image.css' | asset_url | stylesheet_tag }}

{%- liquid
  assign hero_image = section.settings.image
  assign overlay_opacity = section.settings.overlay_opacity | divided_by: 100.0
  assign text_color = section.settings.text_color
  assign content_position = section.settings.content_position
-%}

<section
  class="hero-image hero-image--{{ content_position }}"
  style="
    --hero-min-height: {{ section.settings.min_height }}px;
    --hero-text-color: {{ text_color }};
    --hero-overlay-opacity: {{ overlay_opacity }};
  "
>
  {%- if hero_image != blank -%}
    <div class="hero-image__media">
      {{
        hero_image
        | image_url: width: 1920
        | image_tag:
          srcset: '768, 1200, 1920',
          sizes: '100vw',
          loading: 'eager',
          fetchpriority: 'high',
          class: 'hero-image__img',
          alt: hero_image.alt
      }}
    </div>
  {%- else -%}
    <div class="hero-image__placeholder">
      {{ 'lifestyle-1' | placeholder_svg_tag: 'hero-image__placeholder-svg' }}
    </div>
  {%- endif -%}

  <div class="hero-image__overlay"></div>

  <div class="hero-image__content page-width">
    {%- if section.settings.subheading != blank -%}
      <p class="hero-image__subheading">{{ section.settings.subheading }}</p>
    {%- endif -%}

    {%- if section.settings.heading != blank -%}
      <h1 class="hero-image__heading">{{ section.settings.heading }}</h1>
    {%- endif -%}

    {%- if section.settings.description != blank -%}
      <p class="hero-image__description">{{ section.settings.description }}</p>
    {%- endif -%}

    {%- if section.settings.button_label != blank -%}
      <a
        href="{{ section.settings.button_link }}"
        class="hero-image__button"
      >
        {{ section.settings.button_label }}
      </a>
    {%- endif -%}
  </div>
</section>

{% schema %}
{
  "name": "Hero Image",
  "tag": "section",
  "class": "hero-image-section",
  "settings": [
    {
      "type": "header",
      "content": "Image"
    },
    {
      "type": "image_picker",
      "id": "image",
      "label": "Background image"
    },
    {
      "type": "range",
      "id": "min_height",
      "min": 300,
      "max": 900,
      "step": 50,
      "default": 550,
      "unit": "px",
      "label": "Minimum height"
    },
    {
      "type": "range",
      "id": "overlay_opacity",
      "min": 0,
      "max": 90,
      "step": 5,
      "default": 35,
      "unit": "%",
      "label": "Overlay opacity"
    },
    {
      "type": "header",
      "content": "Content"
    },
    {
      "type": "text",
      "id": "subheading",
      "label": "Subheading",
      "default": "New Arrival"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Your headline here"
    },
    {
      "type": "textarea",
      "id": "description",
      "label": "Description"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Shop Now"
    },
    {
      "type": "url",
      "id": "button_link",
      "label": "Button link"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text color",
      "default": "#ffffff"
    },
    {
      "type": "select",
      "id": "content_position",
      "label": "Content position",
      "options": [
        { "value": "top-left", "label": "Top left" },
        { "value": "top-center", "label": "Top center" },
        { "value": "center-left", "label": "Center left" },
        { "value": "center", "label": "Center" },
        { "value": "center-right", "label": "Center right" },
        { "value": "bottom-left", "label": "Bottom left" },
        { "value": "bottom-center", "label": "Bottom center" }
      ],
      "default": "center"
    }
  ],
  "presets": [
    {
      "name": "Hero Image"
    }
  ]
}
{% endschema %}

The CSS

Create assets/section-hero-image.css:

csscss
.hero-image {
  position: relative;
  display: flex;
  min-height: var(--hero-min-height, 550px);
  overflow: hidden;
  color: var(--hero-text-color, #fff);
}

.hero-image__media,
.hero-image__placeholder {
  position: absolute;
  inset: 0;
}

.hero-image__img,
.hero-image__placeholder-svg {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hero-image__overlay {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: var(--hero-overlay-opacity, 0.35);
  pointer-events: none;
}

.hero-image__content {
  position: relative;
  z-index: 2;
  max-width: 680px;
  padding: 3rem 1.5rem;
}

/* Content positioning */
.hero-image--center {
  align-items: center;
  justify-content: center;
  text-align: center;
}
.hero-image--center .hero-image__content { margin: 0 auto; }

.hero-image--center-left {
  align-items: center;
  justify-content: flex-start;
}

.hero-image--center-right {
  align-items: center;
  justify-content: flex-end;
}

.hero-image--top-left {
  align-items: flex-start;
  justify-content: flex-start;
}

.hero-image--top-center {
  align-items: flex-start;
  justify-content: center;
  text-align: center;
}
.hero-image--top-center .hero-image__content { margin: 0 auto; }

.hero-image--bottom-left {
  align-items: flex-end;
  justify-content: flex-start;
}

.hero-image--bottom-center {
  align-items: flex-end;
  justify-content: center;
  text-align: center;
}
.hero-image--bottom-center .hero-image__content { margin: 0 auto; }

.hero-image__subheading {
  font-size: 0.8rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-bottom: 0.75rem;
  opacity: 0.85;
}

.hero-image__heading {
  font-size: clamp(2rem, 5vw, 3.75rem);
  line-height: 1.08;
  margin-bottom: 1rem;
  font-weight: 700;
}

.hero-image__description {
  font-size: 1.125rem;
  line-height: 1.6;
  margin-bottom: 2rem;
  opacity: 0.9;
}

.hero-image__button {
  display: inline-block;
  padding: 1rem 2.25rem;
  background: #fff;
  color: #000;
  text-decoration: none;
  font-weight: 600;
  font-size: 0.9rem;
  letter-spacing: 0.05em;
  border-radius: 4px;
  transition: background 0.2s, transform 0.2s;
}

.hero-image__button:hover {
  background: #f0f0f0;
  transform: translateY(-1px);
}

@media (max-width: 749px) {
  .hero-image {
    min-height: 400px;
  }

  .hero-image__content {
    padding: 2rem 1rem;
  }
}

Key Decisions in This Code

`loading="eager"` and `fetchpriority="high"` — The hero image is almost always the Largest Contentful Paint (LCP) element. Setting these attributes tells the browser to download it immediately instead of deferring. We'll cover LCP optimization in detail later.

Placeholder SVG fallback — The placeholder_svg_tag filter renders a placeholder when no image is selected, so the section looks presentable in the theme editor before configuration.

CSS custom properties for Liquid values — Instead of inline styles scattered throughout the HTML, we pass Liquid values as CSS variables on the root element. This keeps the markup clean and makes the CSS more maintainable.

Nine-position content grid — The content_position select setting maps to CSS flex positioning. This gives merchants genuine layout control without needing separate section files for each alignment.

Video Background Hero Section

Video heroes grab attention in a way static images cannot. They're especially effective for brands in fashion, food, fitness, and lifestyle verticals where motion communicates the product experience better than a still frame.

Create sections/hero-video.liquid:

liquidliquid
{{ 'section-hero-video.css' | asset_url | stylesheet_tag }}

{%- liquid
  assign video_url = section.settings.video_url
  assign has_video = false
  if video_url != blank
    assign has_video = true
  endif
-%}

<section
  class="hero-video"
  style="
    --hero-min-height: {{ section.settings.min_height }}px;
    --hero-overlay-opacity: {{ section.settings.overlay_opacity | divided_by: 100.0 }};
  "
>
  {%- if has_video -%}
    <div class="hero-video__media">
      <video
        autoplay
        muted
        loop
        playsinline
        preload="auto"
        class="hero-video__player"
        poster="{{ section.settings.fallback_image | image_url: width: 1920 }}"
      >
        {%- for source in section.settings.video_url.sources -%}
          <source src="{{ source.url }}" type="{{ source.mime_type }}">
        {%- endfor -%}
      </video>
    </div>
  {%- elsif section.settings.fallback_image != blank -%}
    <div class="hero-video__media">
      {{
        section.settings.fallback_image
        | image_url: width: 1920
        | image_tag:
          loading: 'eager',
          fetchpriority: 'high',
          class: 'hero-video__fallback-img',
          sizes: '100vw',
          alt: section.settings.fallback_image.alt
      }}
    </div>
  {%- endif -%}

  <div class="hero-video__overlay"></div>

  <div class="hero-video__content page-width">
    {%- if section.settings.heading != blank -%}
      <h1 class="hero-video__heading">{{ section.settings.heading }}</h1>
    {%- endif -%}

    {%- if section.settings.description != blank -%}
      <p class="hero-video__description">{{ section.settings.description }}</p>
    {%- endif -%}

    <div class="hero-video__buttons">
      {%- if section.settings.button_label_1 != blank -%}
        <a href="{{ section.settings.button_link_1 }}" class="hero-video__button hero-video__button--primary">
          {{ section.settings.button_label_1 }}
        </a>
      {%- endif -%}

      {%- if section.settings.button_label_2 != blank -%}
        <a href="{{ section.settings.button_link_2 }}" class="hero-video__button hero-video__button--secondary">
          {{ section.settings.button_label_2 }}
        </a>
      {%- endif -%}
    </div>
  </div>
</section>

{% schema %}
{
  "name": "Hero Video",
  "tag": "section",
  "class": "hero-video-section",
  "settings": [
    {
      "type": "header",
      "content": "Video"
    },
    {
      "type": "video",
      "id": "video_url",
      "label": "Video",
      "info": "Upload via Settings > Files. Keep under 20MB for fast loading."
    },
    {
      "type": "image_picker",
      "id": "fallback_image",
      "label": "Fallback image",
      "info": "Shown while video loads and on mobile if video is disabled."
    },
    {
      "type": "range",
      "id": "min_height",
      "min": 400,
      "max": 900,
      "step": 50,
      "default": 600,
      "unit": "px",
      "label": "Minimum height"
    },
    {
      "type": "range",
      "id": "overlay_opacity",
      "min": 0,
      "max": 90,
      "step": 5,
      "default": 40,
      "unit": "%",
      "label": "Overlay opacity"
    },
    {
      "type": "header",
      "content": "Content"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Watch the experience"
    },
    {
      "type": "textarea",
      "id": "description",
      "label": "Description"
    },
    {
      "type": "text",
      "id": "button_label_1",
      "label": "Primary button label",
      "default": "Shop Now"
    },
    {
      "type": "url",
      "id": "button_link_1",
      "label": "Primary button link"
    },
    {
      "type": "text",
      "id": "button_label_2",
      "label": "Secondary button label"
    },
    {
      "type": "url",
      "id": "button_link_2",
      "label": "Secondary button link"
    }
  ],
  "presets": [
    {
      "name": "Hero Video"
    }
  ]
}
{% endschema %}

Video Hero CSS

Create assets/section-hero-video.css:

csscss
.hero-video {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--hero-min-height, 600px);
  overflow: hidden;
  color: #fff;
  text-align: center;
}

.hero-video__media {
  position: absolute;
  inset: 0;
}

.hero-video__player,
.hero-video__fallback-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hero-video__overlay {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: var(--hero-overlay-opacity, 0.4);
  pointer-events: none;
}

.hero-video__content {
  position: relative;
  z-index: 2;
  max-width: 720px;
  padding: 3rem 1.5rem;
}

.hero-video__heading {
  font-size: clamp(2.25rem, 5.5vw, 4rem);
  line-height: 1.05;
  margin-bottom: 1.25rem;
  font-weight: 700;
}

.hero-video__description {
  font-size: 1.2rem;
  line-height: 1.6;
  opacity: 0.9;
  margin-bottom: 2rem;
}

.hero-video__buttons {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap;
}

.hero-video__button {
  display: inline-block;
  padding: 1rem 2rem;
  text-decoration: none;
  font-weight: 600;
  font-size: 0.9rem;
  letter-spacing: 0.04em;
  border-radius: 4px;
  transition: all 0.2s;
}

.hero-video__button--primary {
  background: #fff;
  color: #000;
}

.hero-video__button--secondary {
  background: transparent;
  color: #fff;
  border: 2px solid rgba(255, 255, 255, 0.6);
}

.hero-video__button--primary:hover { background: #f0f0f0; }
.hero-video__button--secondary:hover { border-color: #fff; }

Video Performance Considerations

Video heroes carry real performance tradeoffs. Here's what to keep in mind:

  • File size — Keep videos under 15-20MB. Compress with H.264 at 1080p and a bitrate around 2-4 Mbps.
  • `poster` attribute — Always set a fallback image as the poster. It displays instantly while the video buffers.
  • `playsinline` — Required for autoplay on iOS. Without it, Safari opens the video in fullscreen.
  • `preload="auto"` — Tells the browser to start downloading the video immediately. Use "metadata" instead if your hero isn't always above the fold.
  • Mobile consideration — Many stores disable autoplay video on mobile to save bandwidth. You can use a media query or JavaScript to swap the video for a static image below a breakpoint.

If you're optimizing for Core Web Vitals on Shopify, video heroes need extra attention because the LCP will be measured against the poster image or first video frame rather than the video itself.

Split-Layout Hero: Image and Text Side by Side

Split heroes divide the viewport into two halves — content on one side, imagery on the other. This pattern works exceptionally well for product launches, about pages, and brands that want a more editorial feel. It naturally provides more space for copy than overlay-based designs.

Create sections/hero-split.liquid:

liquidliquid
{{ 'section-hero-split.css' | asset_url | stylesheet_tag }}

{%- liquid
  assign image_position = section.settings.image_position
  assign bg_color = section.settings.background_color
  assign text_color = section.settings.text_color
-%}

<section
  class="hero-split hero-split--image-{{ image_position }}"
  style="
    --split-bg: {{ bg_color }};
    --split-text: {{ text_color }};
    --split-min-height: {{ section.settings.min_height }}px;
  "
>
  <div class="hero-split__content">
    <div class="hero-split__content-inner">
      {%- if section.settings.subheading != blank -%}
        <p class="hero-split__subheading">{{ section.settings.subheading }}</p>
      {%- endif -%}

      {%- if section.settings.heading != blank -%}
        <h1 class="hero-split__heading">{{ section.settings.heading }}</h1>
      {%- endif -%}

      {%- if section.settings.description != blank -%}
        <div class="hero-split__description">{{ section.settings.description }}</div>
      {%- endif -%}

      {%- if section.settings.button_label != blank -%}
        <a href="{{ section.settings.button_link }}" class="hero-split__button">
          {{ section.settings.button_label }}
        </a>
      {%- endif -%}
    </div>
  </div>

  <div class="hero-split__media">
    {%- if section.settings.image != blank -%}
      {{
        section.settings.image
        | image_url: width: 1200
        | image_tag:
          srcset: '600, 900, 1200',
          sizes: '(max-width: 749px) 100vw, 50vw',
          loading: 'eager',
          fetchpriority: 'high',
          class: 'hero-split__img',
          alt: section.settings.image.alt
      }}
    {%- else -%}
      {{ 'product-1' | placeholder_svg_tag: 'hero-split__placeholder' }}
    {%- endif -%}
  </div>
</section>

{% schema %}
{
  "name": "Hero Split",
  "tag": "section",
  "class": "hero-split-section",
  "settings": [
    {
      "type": "header",
      "content": "Layout"
    },
    {
      "type": "select",
      "id": "image_position",
      "label": "Image position",
      "options": [
        { "value": "right", "label": "Right" },
        { "value": "left", "label": "Left" }
      ],
      "default": "right"
    },
    {
      "type": "range",
      "id": "min_height",
      "min": 400,
      "max": 800,
      "step": 50,
      "default": 550,
      "unit": "px",
      "label": "Minimum height"
    },
    {
      "type": "header",
      "content": "Image"
    },
    {
      "type": "image_picker",
      "id": "image",
      "label": "Image"
    },
    {
      "type": "header",
      "content": "Content"
    },
    {
      "type": "text",
      "id": "subheading",
      "label": "Subheading"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "New collection title"
    },
    {
      "type": "richtext",
      "id": "description",
      "label": "Description"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Explore"
    },
    {
      "type": "url",
      "id": "button_link",
      "label": "Button link"
    },
    {
      "type": "header",
      "content": "Colors"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background color",
      "default": "#f5f5f0"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text color",
      "default": "#1a1a1a"
    }
  ],
  "presets": [
    {
      "name": "Hero Split"
    }
  ]
}
{% endschema %}

Split Hero CSS

Create assets/section-hero-split.css:

csscss
.hero-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  min-height: var(--split-min-height, 550px);
  background: var(--split-bg, #f5f5f0);
  color: var(--split-text, #1a1a1a);
}

.hero-split--image-left .hero-split__media { order: -1; }

.hero-split__content {
  display: flex;
  align-items: center;
  padding: 3rem;
}

.hero-split__content-inner {
  max-width: 520px;
}

.hero-split__subheading {
  font-size: 0.8rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  margin-bottom: 1rem;
  opacity: 0.7;
}

.hero-split__heading {
  font-size: clamp(1.75rem, 3.5vw, 3rem);
  line-height: 1.1;
  margin-bottom: 1.25rem;
  font-weight: 700;
}

.hero-split__description {
  font-size: 1.05rem;
  line-height: 1.7;
  margin-bottom: 2rem;
  opacity: 0.8;
}

.hero-split__button {
  display: inline-block;
  padding: 0.875rem 2rem;
  background: var(--split-text, #1a1a1a);
  color: var(--split-bg, #f5f5f0);
  text-decoration: none;
  font-weight: 600;
  font-size: 0.9rem;
  border-radius: 4px;
  transition: opacity 0.2s;
}

.hero-split__button:hover { opacity: 0.85; }

.hero-split__media {
  position: relative;
  overflow: hidden;
}

.hero-split__img,
.hero-split__placeholder {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (max-width: 749px) {
  .hero-split {
    grid-template-columns: 1fr;
  }

  .hero-split__media {
    min-height: 300px;
    order: -1;
  }

  .hero-split__content {
    padding: 2rem 1.5rem;
  }
}

The sizes="(max-width: 749px) 100vw, 50vw" attribute is critical here. Since the image only takes half the viewport on desktop, telling the browser it's 50vw prevents it from downloading a full-width image when a half-width version suffices. This can cut image payload by 50% or more on desktop.

If you're weighing the tradeoffs between building this yourself and using a page builder, tools like GemPages and PageFly offer drag-and-drop hero builders. They trade flexibility for speed — useful for merchants who need results without code, less useful for developers who need pixel-perfect control.

Overlay and Gradient Techniques That Actually Work

Two stacked holographic UI panels showing responsive layout adaptation between a mobile phone and a tablet wireframe.

The overlay between the background media and the text content is the single most underrated detail in hero design. A bad overlay makes text illegible. A good one creates depth and drama. Here are four patterns you can mix and match.

Solid Color Overlay

The simplest approach. A black overlay at 30-50% opacity works on almost any image:

csscss
.hero__overlay--solid {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, var(--hero-overlay-opacity, 0.4));
}

Gradient Overlay (Bottom Fade)

Better for heroes where you want the top of the image visible and text anchored to the bottom:

csscss
.hero__overlay--gradient-bottom {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.7) 0%,
    rgba(0, 0, 0, 0.3) 40%,
    transparent 100%
  );
}

Directional Gradient (Left to Right)

Pairs naturally with left-aligned text. The gradient darkens the text side while leaving the image clear on the opposite side:

csscss
.hero__overlay--gradient-left {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to right,
    rgba(0, 0, 0, 0.65) 0%,
    rgba(0, 0, 0, 0.2) 50%,
    transparent 100%
  );
}

Brand Color Overlay

Use your brand color instead of black for a cohesive look. This works especially well with lighter images:

csscss
.hero__overlay--brand {
  position: absolute;
  inset: 0;
  background: rgba(26, 78, 64, 0.6);
  mix-blend-mode: multiply;
}

The mix-blend-mode: multiply is the key detail. It blends the color overlay with the underlying image instead of just sitting on top of it, creating a much more natural tinted effect.

Making Overlays Merchant-Configurable

To give merchants control over which overlay style to use, add a select setting to your schema:

jsonjson
{
  "type": "select",
  "id": "overlay_style",
  "label": "Overlay style",
  "options": [
    { "value": "solid", "label": "Solid" },
    { "value": "gradient-bottom", "label": "Gradient (bottom)" },
    { "value": "gradient-left", "label": "Gradient (left)" },
    { "value": "brand", "label": "Brand color" }
  ],
  "default": "solid"
}

Then apply it conditionally in Liquid:

liquidliquid
<div class="hero__overlay hero__overlay--{{ section.settings.overlay_style }}"></div>

This pattern — using a schema select to toggle CSS classes — is one of the most powerful techniques in Shopify theme development. It gives merchants real design choices without exposing raw CSS values. For more on how to leverage this in theme design across your entire store, we cover the full pattern library in a separate guide.

Responsive Design and Mobile-First Hero Optimization

Sleek holographic dashboard illustrating a simplified content delivery network visualization with glowing electric blue nodes.

A hero that looks stunning on a 27-inch monitor and broken on an iPhone is worse than no hero at all. Over 70% of Shopify traffic is mobile. Your hero must be mobile-first.

The Core Responsive Patterns

1. Fluid Typography With `clamp()`

Never use fixed font-size values for hero headings. Use clamp() to set a minimum, preferred, and maximum size:

csscss
.hero__heading {
  /* Minimum 1.75rem, scales with viewport, caps at 3.75rem */
  font-size: clamp(1.75rem, 5vw, 3.75rem);
  line-height: 1.08;
}

.hero__description {
  font-size: clamp(0.95rem, 2vw, 1.2rem);
  line-height: 1.6;
}

2. Separate Mobile and Desktop Images

A wide landscape hero image gets cropped awkwardly on mobile. The best solution is serving different images per breakpoint via the <picture> element:

liquidliquid
{%- if section.settings.mobile_image != blank -%}
  <picture class="hero__media">
    <source
      media="(min-width: 750px)"
      srcset="{{ section.settings.image | image_url: width: 1920 }}"
    >
    <img
      src="{{ section.settings.mobile_image | image_url: width: 750 }}"
      alt="{{ section.settings.mobile_image.alt | escape }}"
      loading="eager"
      fetchpriority="high"
      width="750"
      height="{{ 750 | divided_by: section.settings.mobile_image.aspect_ratio | round }}"
      class="hero__img"
    >
  </picture>
{%- endif -%}

Add a mobile_image setting to your schema:

jsonjson
{
  "type": "image_picker",
  "id": "mobile_image",
  "label": "Mobile image",
  "info": "Recommended: 750 x 900px. Falls back to desktop image if empty."
}

3. Reduce Hero Height on Mobile

A 600px hero on mobile pushes all content below the fold. Use media queries or CSS clamp():

csscss
.hero {
  min-height: clamp(350px, 60vh, 700px);
}

This gives you 350px minimum on small screens, scales with viewport height, and caps at 700px on large screens — all in one line.

4. Stack Split Layouts Vertically

The split hero CSS we wrote earlier already handles this. On mobile, the grid shifts from two columns to one, and the image moves above the text via order: -1. This maintains the visual hierarchy (image first, then CTA) on small screens.

Mobile Hero Comparison Table

PatternDesktopMobileBest For
Full-width imageLarge hero, overlay textShorter height, tighter paddingBrand storytelling
Video backgroundAutoplay videoStatic poster imageLifestyle brands
Split layout50/50 gridStacked, image on topProduct launches
Minimal text-onlyLarge centered typeSmaller type, more paddingEditorial brands

Preloading the LCP Image for Performance

The hero image is almost always the Largest Contentful Paint element on your homepage. LCP is one of Google's three Core Web Vitals, and the threshold for a "good" score is 2.5 seconds. A poorly loaded hero image can single-handedly push your LCP above that threshold and hurt your search rankings.

What Slows Down Hero LCP

Most LCP problems in Shopify heroes come from three sources:

  1. Lazy loading the hero image — Adding loading="lazy" to your above-the-fold hero is the single most common LCP mistake. The browser intentionally delays the download.
  2. Missing `fetchpriority` — Without fetchpriority="high", the browser treats the hero image the same as every other resource.
  3. CSS background images — Using background-image in CSS means the browser can't discover the image until the CSS is parsed. This adds an extra round trip.

The Correct Approach

Every hero code example in this tutorial already follows best practices, but here's the checklist:

liquidliquid
{%- comment -%} Always use an <img> tag, not CSS background-image {%- endcomment -%}
{{
  section.settings.image
  | image_url: width: 1920
  | image_tag:
    loading: 'eager',
    fetchpriority: 'high',
    sizes: '100vw',
    alt: section.settings.image.alt
}}
  • `loading: 'eager'` — Overrides Shopify's default lazy loading for images. Only use this for your hero; every other image on the page should remain lazy.
  • `fetchpriority: 'high'` — Tells the browser this is the most important image to download. Google's case studies show this single attribute can improve LCP by up to 700ms.
  • `<img>` over `background-image` — HTML image elements are discoverable during the HTML parse phase. CSS background images require the stylesheet to be downloaded and parsed first.

Preloading With Shopify's image_tag Filter

Shopify's image_tag Liquid filter can automatically generate preload hints. When used in combination with the section's section.index property, you can conditionally preload only the first section on the page:

liquidliquid
{%- if section.index == 1 -%}
  <link
    rel="preload"
    as="image"
    href="{{ section.settings.image | image_url: width: 1920 }}"
    imagesrcset="
      {{ section.settings.image | image_url: width: 768 }} 768w,
      {{ section.settings.image | image_url: width: 1200 }} 1200w,
      {{ section.settings.image | image_url: width: 1920 }} 1920w
    "
    imagesizes="100vw"
    fetchpriority="high"
  >
{%- endif -%}

Using section.index == 1 ensures the preload link only renders for the very first section on the page. Without this guard, every hero section on a page with multiple heroes would generate preload hints, which defeats the purpose.

For a full breakdown of Shopify performance tuning beyond the hero, our guide on Shopify Core Web Vitals optimization covers LCP, CLS, and INP across your entire storefront. Shopify's own performance best practices documentation is also essential reading.

Hero Section With Countdown Timer

Close-up of two identical holographic wireframes side-by-side on a dark background showing A/B testing variants.

Countdown timers create urgency. They're commonly used during sales, product launches, and limited-time offers. Here's a hero section with a built-in countdown that's fully configurable through the theme editor.

Create sections/hero-countdown.liquid:

liquidliquid
{{ 'section-hero-countdown.css' | asset_url | stylesheet_tag }}

<section
  class="hero-countdown"
  style="
    --hero-min-height: {{ section.settings.min_height }}px;
    --hero-overlay-opacity: {{ section.settings.overlay_opacity | divided_by: 100.0 }};
  "
>
  {%- if section.settings.image != blank -%}
    <div class="hero-countdown__media">
      {{
        section.settings.image
        | image_url: width: 1920
        | image_tag:
          loading: 'eager',
          fetchpriority: 'high',
          sizes: '100vw',
          class: 'hero-countdown__img',
          alt: section.settings.image.alt
      }}
    </div>
  {%- endif -%}

  <div class="hero-countdown__overlay"></div>

  <div class="hero-countdown__content page-width">
    {%- if section.settings.heading != blank -%}
      <h1 class="hero-countdown__heading">{{ section.settings.heading }}</h1>
    {%- endif -%}

    {%- if section.settings.description != blank -%}
      <p class="hero-countdown__description">{{ section.settings.description }}</p>
    {%- endif -%}

    <div
      class="hero-countdown__timer"
      data-countdown-target="{{ section.settings.end_date }}T{{ section.settings.end_time }}:00"
      data-countdown-expired="{{ section.settings.expired_message }}"
    >
      <div class="hero-countdown__unit">
        <span class="hero-countdown__number" data-countdown-days>00</span>
        <span class="hero-countdown__label">Days</span>
      </div>
      <div class="hero-countdown__separator">:</div>
      <div class="hero-countdown__unit">
        <span class="hero-countdown__number" data-countdown-hours>00</span>
        <span class="hero-countdown__label">Hours</span>
      </div>
      <div class="hero-countdown__separator">:</div>
      <div class="hero-countdown__unit">
        <span class="hero-countdown__number" data-countdown-minutes>00</span>
        <span class="hero-countdown__label">Min</span>
      </div>
      <div class="hero-countdown__separator">:</div>
      <div class="hero-countdown__unit">
        <span class="hero-countdown__number" data-countdown-seconds>00</span>
        <span class="hero-countdown__label">Sec</span>
      </div>
    </div>

    {%- if section.settings.button_label != blank -%}
      <a href="{{ section.settings.button_link }}" class="hero-countdown__button">
        {{ section.settings.button_label }}
      </a>
    {%- endif -%}
  </div>
</section>

<script>
  (function () {
    const timer = document.querySelector(
      '.hero-countdown__timer[data-countdown-target]'
    );
    if (!timer) return;

    const target = new Date(timer.dataset.countdownTarget).getTime();
    const expiredMsg = timer.dataset.countdownExpired || 'Offer ended';

    function update() {
      const now = Date.now();
      const diff = target - now;

      if (diff <= 0) {
        timer.innerHTML = `<p class="hero-countdown__expired">${expiredMsg}</p>`;
        return;
      }

      const days = Math.floor(diff / 86400000);
      const hours = Math.floor((diff % 86400000) / 3600000);
      const minutes = Math.floor((diff % 3600000) / 60000);
      const seconds = Math.floor((diff % 60000) / 1000);

      timer.querySelector('[data-countdown-days]').textContent = String(days).padStart(2, '0');
      timer.querySelector('[data-countdown-hours]').textContent = String(hours).padStart(2, '0');
      timer.querySelector('[data-countdown-minutes]').textContent = String(minutes).padStart(2, '0');
      timer.querySelector('[data-countdown-seconds]').textContent = String(seconds).padStart(2, '0');

      requestAnimationFrame(() => setTimeout(update, 1000));
    }

    update();
  })();
</script>

{% schema %}
{
  "name": "Hero Countdown",
  "tag": "section",
  "class": "hero-countdown-section",
  "settings": [
    {
      "type": "image_picker",
      "id": "image",
      "label": "Background image"
    },
    {
      "type": "range",
      "id": "min_height",
      "min": 400,
      "max": 800,
      "step": 50,
      "default": 550,
      "unit": "px",
      "label": "Minimum height"
    },
    {
      "type": "range",
      "id": "overlay_opacity",
      "min": 0,
      "max": 90,
      "step": 5,
      "default": 45,
      "unit": "%",
      "label": "Overlay opacity"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Sale ends soon"
    },
    {
      "type": "textarea",
      "id": "description",
      "label": "Description"
    },
    {
      "type": "header",
      "content": "Countdown"
    },
    {
      "type": "text",
      "id": "end_date",
      "label": "End date",
      "info": "Format: YYYY-MM-DD",
      "default": "2026-12-31"
    },
    {
      "type": "text",
      "id": "end_time",
      "label": "End time",
      "info": "Format: HH:MM (24-hour)",
      "default": "23:59"
    },
    {
      "type": "text",
      "id": "expired_message",
      "label": "Expired message",
      "default": "This offer has ended"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Shop the Sale"
    },
    {
      "type": "url",
      "id": "button_link",
      "label": "Button link"
    }
  ],
  "presets": [
    {
      "name": "Hero Countdown"
    }
  ]
}
{% endschema %}

Countdown Timer CSS

Add these styles to assets/section-hero-countdown.css:

csscss
.hero-countdown {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--hero-min-height, 550px);
  overflow: hidden;
  color: #fff;
  text-align: center;
}

.hero-countdown__media { position: absolute; inset: 0; }
.hero-countdown__img { width: 100%; height: 100%; object-fit: cover; }
.hero-countdown__overlay {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: var(--hero-overlay-opacity, 0.45);
}

.hero-countdown__content {
  position: relative;
  z-index: 2;
  padding: 3rem 1.5rem;
  max-width: 700px;
}

.hero-countdown__heading {
  font-size: clamp(2rem, 5vw, 3.5rem);
  margin-bottom: 0.75rem;
  font-weight: 700;
}

.hero-countdown__description {
  font-size: 1.1rem;
  opacity: 0.9;
  margin-bottom: 2rem;
}

.hero-countdown__timer {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 2.5rem;
}

.hero-countdown__unit { text-align: center; }

.hero-countdown__number {
  display: block;
  font-size: clamp(2rem, 6vw, 3.5rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.hero-countdown__label {
  display: block;
  font-size: 0.7rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  opacity: 0.7;
  margin-top: 0.25rem;
}

.hero-countdown__separator {
  font-size: 2rem;
  font-weight: 300;
  opacity: 0.5;
  align-self: flex-start;
  padding-top: 0.25rem;
}

.hero-countdown__expired {
  font-size: 1.25rem;
  opacity: 0.8;
}

.hero-countdown__button {
  display: inline-block;
  padding: 1rem 2.25rem;
  background: #fff;
  color: #000;
  text-decoration: none;
  font-weight: 600;
  border-radius: 4px;
  transition: background 0.2s;
}

.hero-countdown__button:hover { background: #f0f0f0; }

Note the use of font-variant-numeric: tabular-nums on the countdown numbers. Without this, digits shift horizontally as they change because proportional numerals have different widths. Tabular numerals give every digit the same width, eliminating the visual jitter.

A/B Testing Hero Variants With Liquid

Complex 3D holographic flow chart with glowing electric blue lines and nodes on a dark background.

You don't have to guess which hero converts better. Shopify's Liquid gives you enough tools to run basic A/B tests directly in your theme code — no third-party app required.

The Random Assignment Pattern

The simplest approach uses Liquid's | modulo filter against a timestamp to randomly assign visitors to a variant:

liquidliquid
{%- liquid
  assign timestamp = 'now' | date: '%S'
  assign variant = timestamp | modulo: 2
-%}

{%- if variant == 0 -%}
  {%- comment -%} Variant A: Image hero {%- endcomment -%}
  {%- render 'hero-variant-a' -%}
{%- else -%}
  {%- comment -%} Variant B: Video hero {%- endcomment -%}
  {%- render 'hero-variant-b' -%}
{%- endif -%}

This is a rough 50/50 split. It's not statistically rigorous — the assignment isn't sticky across sessions, and Shopify's page caching can skew results — but it's a fast way to test two layouts.

A More Robust Approach Using Blocks

For a merchant-friendly A/B test, use blocks to define variants and JavaScript to handle sticky assignment:

liquidliquid
{%- if section.blocks.size > 0 -%}
  <div class="hero-ab-test" data-hero-ab>
    {%- for block in section.blocks -%}
      <div
        class="hero-ab-test__variant"
        data-variant-index="{{ forloop.index0 }}"
        style="display: none;"
        {{ block.shopify_attributes }}
      >
        {%- if block.settings.image != blank -%}
          <section class="hero-image" style="--hero-min-height: 550px;">
            <div class="hero-image__media">
              {{
                block.settings.image
                | image_url: width: 1920
                | image_tag:
                  loading: 'eager',
                  sizes: '100vw',
                  class: 'hero-image__img',
                  alt: block.settings.image.alt
              }}
            </div>
            <div class="hero-image__overlay" style="opacity: 0.35;"></div>
            <div class="hero-image__content" style="color: #fff;">
              {%- if block.settings.heading != blank -%}
                <h1 class="hero-image__heading">{{ block.settings.heading }}</h1>
              {%- endif -%}
              {%- if block.settings.button_label != blank -%}
                <a href="{{ block.settings.button_link }}" class="hero-image__button">
                  {{ block.settings.button_label }}
                </a>
              {%- endif -%}
            </div>
          </section>
        {%- endif -%}
      </div>
    {%- endfor -%}
  </div>

  <script>
    (function () {
      const container = document.querySelector('[data-hero-ab]');
      if (!container) return;

      const variants = container.querySelectorAll('[data-variant-index]');
      const totalVariants = variants.length;
      if (totalVariants === 0) return;

      // Sticky assignment via localStorage
      let assigned = localStorage.getItem('hero_ab_variant');
      if (assigned === null || assigned >= totalVariants) {
        assigned = Math.floor(Math.random() * totalVariants);
        localStorage.setItem('hero_ab_variant', assigned);
      }

      variants[assigned].style.display = '';

      // Track impression for analytics
      if (window.Shopify && window.Shopify.analytics) {
        window.Shopify.analytics.publish('hero_ab_impression', {
          variant: assigned,
        });
      }
    })();
  </script>
{%- endif -%}

What to A/B Test

Not everything in a hero section is worth testing. Focus on changes that are likely to move the needle:

ElementLow ImpactHigh Impact
HeadlineChanging a single wordDifferent value proposition entirely
CTA buttonColor changeDifferent action (Shop vs. Learn More)
ImageSimilar product angleLifestyle vs. product shot
LayoutAlignment tweaksFull-width vs. split layout
OfferFree shipping badgeDiscount percentage in headline

For serious A/B testing at scale, dedicated tools like Instant let you build and test multiple landing page variants with built-in analytics. But the Liquid approach above costs nothing and gives you directional data for your conversion optimization efforts.

Choosing the Right Hero Pattern for Your Store

Not every store needs a video hero. Not every brand benefits from a countdown timer. The right hero pattern depends on your product, your audience, and your goals. Here's a decision framework based on what we've seen work across hundreds of Shopify stores.

By Store Type

Store TypeRecommended HeroWhy
Fashion / ApparelFull-width image or videoLifestyle imagery sells the aesthetic
Electronics / TechSplit layoutRoom for spec highlights alongside product image
Food / BeverageVideo backgroundMotion conveys freshness and preparation
Limited drops / HypeCountdown timer heroScarcity drives urgency
B2B / WholesaleSplit layoutProfessional tone, space for detailed copy
Single-product brandFull-width imageOne product, one statement, one CTA

By Business Goal

  • Brand awareness — Full-width image or video with minimal text. Let the visuals do the talking.
  • Product launch — Split layout or countdown timer. Combine product imagery with specific details.
  • Sale / promotion — Countdown hero. The timer creates urgency that static text cannot match.
  • Email capture — Any layout with an inline form replacing the CTA button. Pair with an offer.

Performance vs. Visual Impact Tradeoffs

Every hero pattern carries different performance characteristics:

PatternAvg. LCP ImpactPayload SizeMobile Score
Full-width imageLow (fast)80-200 KBExcellent
Video backgroundHigh (slow)5-20 MBPoor without fallback
Split layoutLow (fast)60-150 KBGood
Countdown timerLow (fast)80-200 KB + 2 KB JSGood
Slideshow / carouselMedium200-600 KBPoor (CLS risk)

Notice that slideshows are listed but not recommended. Carousels introduce Cumulative Layout Shift (CLS) issues, require additional JavaScript, and studies consistently show that visitors rarely interact with slides beyond the first one. A single strong hero outperforms a slideshow in almost every metric.

For guidance on building themes that extend well beyond the hero, Litos has a thorough guide on building a complete Shopify theme from scratch, and Shopify's input settings documentation covers every setting type available for your schemas.

Putting It All Together: Your Hero Section Checklist

Before you ship a hero section to production, run through this checklist:

Markup

  • Uses an <img> tag (not CSS background-image) for the hero image
  • Includes loading="eager" and fetchpriority="high" on the hero image
  • Has srcset and sizes attributes for responsive image delivery
  • Includes a placeholder SVG fallback for empty image states
  • Uses semantic HTML (<section>, <h1>, <p>)

Schema

  • image_picker for the background image
  • Separate image_picker for a mobile-specific image
  • range sliders for height and overlay opacity
  • select for content alignment and overlay style
  • color pickers for text and overlay colors
  • presets array so the section appears in the "Add section" picker

CSS

  • Uses clamp() for fluid typography
  • Mobile breakpoint reduces hero height and adjusts padding
  • Content is readable on both light and dark images (overlay handles this)
  • No horizontal overflow on any viewport width
  • Uses font-variant-numeric: tabular-nums on any animated numbers

Performance

  • Hero image is preloaded when the section is first on the page
  • Video hero has a poster image and playsinline attribute
  • No render-blocking JavaScript in the hero section
  • Images are served via Shopify's CDN (using image_url filter, not hardcoded paths)

Every code example in this tutorial is designed to pass this checklist out of the box. Copy a pattern, customize the styles to match your brand, and configure the schema settings to give merchants exactly the controls they need.

If you're building hero sections as part of a broader store build, explore our Shopify development resources for tutorials on app blocks, custom sections, and theme architecture. And if you want to discuss implementation details, post in our community forum — we review hero section code submissions every week.

Ready to build? Pick one of the five patterns above, paste the code into your theme, and ship a hero section that actually converts. The best hero isn't the flashiest — it's the one that loads fast, communicates clearly, and drives visitors to take action.

Shopify DevelopmentTheme DesignConversion Optimization
Talk Shop

About Talk Shop

The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.

Related Insights

Related

How to Sell on Instagram: Complete Guide for Shopify Stores

Related

How to Create a Brand Identity for Your Shopify Store

The ecommerce newsletter that's actually useful.

Daily trends, teardowns, and tactics from the top 1% of ecommerce brands. Delivered every morning.

No spam. Unsubscribe anytime.

Free

SEO Audit Tool

Analyze your store's SEO in seconds. Get a scored report with actionable fixes.

Audit Your Site

Talk Shop Daily

Daily ecommerce news, teardowns, and tactics.

No spam. Unsubscribe anytime.

Try our Free SEO Audit