A hero can look polished while creating basic barriers: two h1 elements on one page, a vague “Learn more” link, text baked into an image, no keyboard focus, or a mobile crop that hides the subject. Accessibility has to be part of the section's content model and markup, not a final overlay.
This tutorial converts a static promotional hero into a merchant-editable Shopify section. The result supports an intentional heading level, plain-text content, an optional descriptive link, responsive image output, visible focus, and a mobile-first layout that preserves DOM reading order.
Prerequisites and design decisions
Work in a Shopify development theme. Before coding, identify the page's existing primary heading. A home template might use this hero's heading as the h1; a hero placed below a collection title should normally use h2. Because a section is reusable, the example exposes a constrained heading-level choice and explains it to the editor.
You should also have an image whose important subject survives narrow crops and useful alt text stored with that image. Shopify's theme accessibility guidance is the primary reference for theme-wide expectations.
Static HTML input
The design starts here:
<section class="hero">
<div class="hero__content">
<p>New season</p>
<h1>Layers for changing weather</h1>
<p>Lightweight pieces that work together from morning to evening.</p>
<a href="/collections/layers">Explore lightweight layers</a>
</div>
<img src="layers.jpg" alt="Two lightweight jackets hanging beside a canvas bag">
</section>
The reading order is already sensible: label, heading, supporting text, link, then image. Keep that order in the DOM. CSS can create a two-column desktop layout without forcing assistive technologies or keyboard users through a different sequence.
Complete accessible hero section
Create sections/accessible-hero.liquid:
{% style %}
#shopify-section-{{ section.id }} .accessible-hero {
display: grid;
color: {{ section.settings.text_color }};
background: {{ section.settings.background_color }};
}
#shopify-section-{{ section.id }} .accessible-hero__content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 1rem;
padding: clamp(2rem, 6vw, 5rem);
}
#shopify-section-{{ section.id }} .accessible-hero__content > * {
margin: 0;
}
#shopify-section-{{ section.id }} .accessible-hero__link {
display: inline-flex;
min-height: 2.75rem;
align-items: center;
padding: 0.75rem 1rem;
color: {{ section.settings.background_color }};
background: {{ section.settings.text_color }};
border: 2px solid currentColor;
border-radius: 0.25rem;
}
#shopify-section-{{ section.id }} .accessible-hero__link:focus-visible {
outline: 3px solid {{ section.settings.focus_color }};
outline-offset: 4px;
}
#shopify-section-{{ section.id }} .accessible-hero__image {
display: block;
width: 100%;
height: 100%;
min-height: 18rem;
object-fit: cover;
}
@media (min-width: 750px) {
#shopify-section-{{ section.id }} .accessible-hero {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
}
@media (prefers-reduced-motion: reduce) {
#shopify-section-{{ section.id }} .accessible-hero * {
scroll-behavior: auto;
transition-duration: 0.01ms;
}
}
{% endstyle %}
{% assign heading_tag = 'h2' %}
{% if section.settings.heading_level == 'h1' %}
{% assign heading_tag = 'h1' %}
{% endif %}
<section
class="accessible-hero"
{% if section.settings.heading != blank %}aria-labelledby="HeroHeading-{{ section.id }}"{% endif %}
>
<div class="accessible-hero__content">
{% if section.settings.eyebrow != blank %}
<p>{{ section.settings.eyebrow | escape }}</p>
{% endif %}
{% if section.settings.heading != blank %}
<{{ heading_tag }} id="HeroHeading-{{ section.id }}">
{{ section.settings.heading | escape }}
</{{ heading_tag }}>
{% endif %}
{% if section.settings.body != blank %}
<div class="rte">{{ section.settings.body }}</div>
{% endif %}
{% if section.settings.link_label != blank and section.settings.link != blank %}
<a class="accessible-hero__link" href="{{ section.settings.link }}">
{{ section.settings.link_label | escape }}
</a>
{% endif %}
</div>
{% if section.settings.image != blank %}
{{
section.settings.image
| image_url: width: 1800
| image_tag:
class: 'accessible-hero__image',
widths: '480, 720, 960, 1200, 1500, 1800',
sizes: '(min-width: 750px) 50vw, 100vw',
loading: 'eager',
alt: section.settings.image.alt
}}
{% endif %}
</section>
{% schema %}
{
"name": "Accessible hero",
"settings": [
{
"type": "text",
"id": "eyebrow",
"label": "Eyebrow",
"default": "New season"
},
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Layers for changing weather"
},
{
"type": "select",
"id": "heading_level",
"label": "Heading level",
"info": "Use H1 only when this is the page's primary heading.",
"options": [
{ "value": "h1", "label": "H1" },
{ "value": "h2", "label": "H2" }
],
"default": "h2"
},
{
"type": "richtext",
"id": "body",
"label": "Text",
"default": "<p>Lightweight pieces that work together from morning to evening.</p>"
},
{
"type": "text",
"id": "link_label",
"label": "Link label",
"default": "Explore lightweight layers"
},
{
"type": "url",
"id": "link",
"label": "Link"
},
{
"type": "image_picker",
"id": "image",
"label": "Image"
},
{
"type": "color",
"id": "background_color",
"label": "Background",
"default": "#111827"
},
{
"type": "color",
"id": "text_color",
"label": "Text",
"default": "#ffffff"
},
{
"type": "color",
"id": "focus_color",
"label": "Focus outline",
"default": "#22d3ee"
}
],
"presets": [
{
"name": "Accessible hero"
}
]
}
{% endschema %}
Heading structure is page-level information
An h1 is not a size setting. It identifies the page's primary subject. The section defaults to h2 so adding it below an existing title does not automatically introduce a competing primary heading. If the hero supplies the home page's only visible primary title, the editor can choose h1.
The section's aria-labelledby points to that visible heading only when the heading is present. An ARIA reference to a missing element is not useful, so the Liquid condition controls both the label relationship and the heading output.
Keep controls understandable
The destination and visible label are separate settings, but the link renders only when both exist. “Explore lightweight layers” describes the destination more clearly out of context than “Learn more.” The example uses a native anchor because it navigates. Do not replace it with a div plus click handler.
The focus rule uses :focus-visible so keyboard focus remains prominent without adding an outline to every pointer click. Check the focus colour against both the section background and the link surface. Merchant-selectable colours can create inaccessible combinations; defaults are only a starting point, so document and test the combinations your theme allows.
Image and responsive behaviour
The image remains after the content in the DOM on all viewports. Mobile shows one column, while desktop uses two equal tracks. image_tag supplies intrinsic dimensions and responsive source candidates. The example loads eagerly because a primary hero is commonly near the top; change it to lazy when the same section is placed below the fold. Do not preload every hero-like section on a template.
Use meaningful image alt text when the image adds information. If the image is purely decorative and all meaning is present in nearby text, store an empty alt value deliberately. Avoid using promotional copy as alt text, and never put the only copy inside the image.
Expected result
With defaults and a selected image, a keyboard user reaches one clearly labelled link with an obvious cyan outline. A screen-reader user hears the eyebrow as ordinary text, the chosen heading level, the body, link, and image alternative in logical order. On mobile, the content is readable before the potentially larger image download completes.
The reduced-motion rule is intentionally defensive; this section contains no animation. If you add parallax, auto-rotation, or video later, provide a real reduced-motion experience rather than relying on an extremely short transition alone.
Common failure cases
- Two page-level headings: inspect the entire template and change the hero to
h2when anotherh1already names the page. - Missing accessible name: do not render a link with a URL but no visible label.
- Invisible focus: verify the outline against every permitted colour combination.
- Text covers the image: overlay designs require stronger contrast management and mobile crop testing; the side-by-side layout avoids that dependency.
- Image blocks mobile content: keep the meaningful content first in DOM order and use responsive source sizes.
- Empty ARIA reference: only use
aria-labelledbywhen the referenced heading renders. - Rich text inside a paragraph: render the
richtextsetting in adivas shown.
Test in a development theme
- Add the section to a page that already has an
h1, then to a home template without one. - Inspect the accessibility tree and heading outline in browser developer tools.
- Navigate the page using only Tab and Shift+Tab; confirm focus is never clipped or hidden.
- Zoom to 200% and check that content reflows without horizontal scrolling.
- Test at 320 pixels wide and with long heading and link text.
- Check default and merchant-selected colours with a contrast analyser.
- Disable images and confirm the remaining text still communicates the offer.
- Enable reduced-motion in the operating system before adding any future motion.
- Run
shopify theme checkand test both the editor preview and a normal storefront URL.
Manual verification checklist
- The template has one intentional primary heading.
- Section content follows a logical DOM order.
- The link label makes sense out of context.
- Keyboard focus is visible against adjacent colours.
- Text and controls meet contrast requirements.
- Image alt text matches the image's purpose.
- Content remains usable without the image.
- Mobile, zoom, long text, and reduced motion are tested.
- The schema parses and the empty-field states are clean.
Start a conversion in the HTML converter, then apply this accessibility review rather than treating generated markup as final. The responsive Shopify images tutorial goes deeper on image delivery, and the Liquid cheatsheet provides shorter syntax references.
Conclusion
An accessible hero is a collection of explicit choices: page-aware heading level, descriptive native controls, useful text independent of imagery, stable reading order, visible focus, tested colours, and responsive media. Encoding those choices into the schema and complete section makes them easier for merchants to preserve.