Internal reference
TierZero Styleguide
Design tokens, components and patterns for this theme. Every value below is read from theme settings, so it always matches the live storefront.
Colour tokens
Read live from theme settings. Use the CSS custom property, never a hex value — hardcoding a colour breaks merchant theming.
-
var(--text-color)#616161 -
var(--heading-color)#16134a -
var(--accent-color)#16134a -
var(--link-color)#c8e9f2 -
var(--border-color)#e8e8e8 -
var(--background)#f5f5f5 -
var(--secondary-background)#ffffff
Typography
Headings use var(--heading-font-family), body copy uses
var(--text-font-family). Base size is
16px.
Heading 1 — Figtree
Heading 2
Heading 3
Heading 4
Heading 5
Body copy — Figtree. The quick brown fox jumps over the lazy dog.
Small print and captions.
Buttons
Shared snippets
section-header
Standard heading block for content sections. Pass heading_tag: 'h2' below a hero.
{%- render 'section-header',
eyebrow: section.settings.eyebrow,
heading: section.settings.heading,
subheading: section.settings.subheading,
heading_tag: 'h2',
alignment: 'left' -%}
promo-link-label
Produces an accessible name for an image-only link, so it does not fail WCAG 2.4.4.
{%- capture promo_label -%}
{%- render 'promo-link-label', title: block.settings.title, image: block.settings.image, link: block.settings.link -%}
{%- endcapture -%}
Responsive images
Always use image_tag: it generates a srcset automatically. Lazy-load
below the fold only — never above it, since that directly harms LCP.
{{ block.settings.image
| image_url: width: 600
| image_tag:
loading: 'lazy',
sizes: '(min-width: 741px) 280px, 45vw',
widths: '200,280,400,560,600',
alt: block.settings.image.alt | default: block.settings.name }}
Do and don't
| Do | Don't |
|---|---|
var(--accent-color) | #17A0EC hardcoded |
Text in section settings or locales/ | Hardcoded strings in markup |
{% render %} | {% include %} (deprecated) |
One h1 per page | Multiple h1 across sections |
Edits in custom.css | Edits in theme.css (vendor) |
| Every link has an accessible name | Image-only links with alt="" |
Spacing & layout
Every custom section exposes padding_top and padding_bottom
range settings in its schema, stepped by 4px. Follow that convention in new sections —
merchants expect to control vertical rhythm without a developer.
{ "type": "range", "id": "padding_top", "label": "Padding top",
"min": 0, "max": 120, "step": 4, "unit": "px", "default": 48 }
Typical defaults in this theme: 48px for a content section,
40px for a dense one, 56px for a hero. Horizontal
gutters come from .container; never add your own page-level side padding.
The fractional grid
Inherited from the Warehouse base theme and used by every product grid. Classes read
as fraction--breakpoint and are passed to snippets as
grid_classes. This syntax cannot be guessed, so it is documented here.
| Class | Meaning |
|---|---|
1/2--tablet | Half width from the tablet breakpoint up |
1/3--lap-and-up | One third from laptop up |
1/4--lap | One quarter at the laptop breakpoint |
1/5--desk | One fifth at desktop |
1/6--wide | One sixth on wide screens |
Combine them to change per breakpoint — they are additive, not exclusive.
Product card
snippets/product-item.liquid — the most reused component in the theme.
Always render products through it rather than building card markup, so price,
badges, membership labels and quick-add behave consistently.
{% render 'product-item',
product: product,
grid_classes: '1/2--tablet 1/3--lap-and-up',
show_add_to_cart: true %}
horizontal: true switches to the list layout used on search results.
Live example — rendered from real store data:
Page sections
Built for editorial pages. All carry presets, so they can be added to any
page from the theme editor — not only the templates they were written for.
| Section | Blocks | Use it for |
|---|---|---|
page-hero | meta | Page h1 plus label/value pairs and a CTA |
spec-table | row | Structured facts as a label/value table |
grade-detail | grade | Paired cards with criteria and front/back photos |
scale-comparison | segment | Comparing scales on one aligned track |
grade-crosswalk | mapping | Equivalence table across systems |
checklist-steps | step | Numbered walkthrough where order matters |
rule-list | rule | Set of short titled rules |
pre-order-hero | meta, trust | Pre-order page h1, release facts, CTA and reassurance row |
release-countdown | — | Release date plus a live countdown |
product-lineup | group | The few products a launch page is actually selling |
release-timeline | — | Ordered dates with passed/upcoming states; steps with no date are skipped |
rarity-breakdown | — | Proportional bar plus counts and percentages, from plain "Label: count" lines |
set-overview | — | Long-form copy with a <details> read-more, for indexable text |
styleguide | — | This page |
Only one section per page may render the h1. page-hero
and pre-order-hero have a Use as page title toggle; everything
below them uses h2. Watch the stock faq section: its
separator block renders an h1, so a page that already has
a hero must use question blocks only.
Data-driven sections
The constraint to understand first: a JSON page template shares its section settings with every page that uses it. Type a release date into a section and all pre-order pages show that date. So anything that varies per page cannot live in a setting — it has to come from data.
The split this theme uses: layout, labels and copy stay in section
settings, and per-release facts come from the collection's
pre_order metafields. Sections name the fact they want; they never hold
the value.
| Step | What happens |
|---|---|
| 1. Resolve the collection | Page's pre_order.collection metafield, else collections[page.handle], else the section's fallback setting. The page metafield wins, because it is the only per-page one. |
| 2. Name a fact | Each block has a Value from select — release_date, price, formats… |
| 3. Resolve it | snippets/pre-order-value.liquid maps the name to the metafield and outputs text, or nothing if unset. |
| 4. Fall back | Metafield → the block's written value → a visible “To confirm” flag. Never a blank cell. |
{%- liquid
assign po_collection = page.metafields.pre_order.collection.value
if po_collection == blank
assign po_collection = collections[page.handle]
endif
if po_collection == blank
assign po_collection = section.settings.collection
endif
-%}
{%- capture row_value -%}
{%- render 'pre-order-value',
po_collection: po_collection,
source: block.settings.source,
date_format: section.settings.date_format -%}
{%- endcapture -%}
{%- assign row_value = row_value | strip -%}
render has isolated scope, so a snippet cannot assign variables for its
caller. That is why the resolver outputs a value and the caller captures it —
and why the eight-line collection lookup is repeated in each section rather than
shared.
Adding a new fact is two edits: a when branch in the resolver and one
option in each schema that should offer it. spec-table
defaults every row to Written below, so existing pages are unaffected.
Do not use featured-collection on a shared template.
Its collection is a section setting, so every page using the template would show the
same collection. Sections that need the page's own collection must resolve it as above.
Repeatable per-page content cannot be blocks either. Blocks live in the
template, so they are shared too. A per-set rarity list or overview has to arrive as
data: rarity-breakdown parses plain Label: count lines and
set-overview splits a multi-line metafield into paragraphs. Plain text
beats JSON or metaobjects here — someone copying four numbers off a publisher page
should not have to learn a syntax.
Check for structured data the theme already emits.
snippets/microdata-schema.liquid puts a BreadcrumbList in the
document head on every page. A second one from a section produced two competing trails
for one URL, so pre-order-hero renders visible breadcrumbs by default and
keeps its JSON-LD behind an off-by-default setting.
Pills
snippets/pill.liquid — badge for grade codes, region tags and short
status labels. Variants tie a pill to the scale it belongs to.
{%- render 'pill', code: 'NM', label: 'Near Mint', variant: 'na' -%}
Component states
A component is not done until every state is handled. Check these before shipping.
| State | Requirement |
|---|---|
| Default | Meets 4.5:1 contrast for text |
| Hover | Visible change that is not colour alone |
| Focus | Never remove the outline without an equally visible replacement |
| Active / pressed | Reflect it in aria-pressed where it is a toggle |
| Disabled | Use the real disabled attribute, not just styling |
| Empty | Say what is missing and how to fix it — never render blank |
| Error | Message next to the field, not only a red border |
Form elements
Every input needs a <label for> pointing at its id.
A placeholder is not a label. Build ids from something stable — an id built from an
out-of-scope Liquid variable renders empty and silently breaks the association.
Icons
{%- render 'icon', icon: 'cart' -%}
Icons are decorative by default. If an icon is the only content of a button or link,
the control still needs an accessible name via aria-label.
Names must match the when cases in snippets/icon.liquid.
An unknown name renders nothing at all — silently, with no error. Available include:
cart, big-cart, search, close,
close-2, minus, check, heart,
account, filter, grid, list,
lock, package, address, play,
timer, zoom, hamburger,
arrow-left, arrow-right, tail-left,
tail-right, plus the social icons.
Accessibility rules
CI fails on link-name and aria-allowed-attr. These are the
rules behind that, each one written from a defect actually found in this theme.
| Rule | Why |
|---|---|
| Every link has an accessible name | 15 image-only links shipped with empty text and alt="" |
aria-expanded only on interactive elements | It was on a plain div, which is invalid |
Table row labels use <th scope="row"> | <td> loses the label/value pairing |
| Never carry meaning by colour alone | Add a border, icon or text as well — WCAG 1.4.1 |
Respect prefers-reduced-motion | Keep the state change, drop the movement |
| Contrast: 4.5:1 normal, 3:1 large text | The brand blue currently fails at 2.89:1 |



