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 -%}

Spacing & layout

Vertical rhythm comes from .section, not from a section's own padding. Measured on the live storefront:

ClassMeasured on this pageUse
.sectionThe default gap between any two sections
.section--tightA denser band, e.g. the trust icons row
.containerEvery section's inner wrapper. Never add page-level side padding of your own

Both are responsive, which is why they are measured rather than written down: .section is 50px below the 641px breakpoint and 60px from there up, and .container's gutter widens with it. Resize the window and the table follows.

Custom sections still expose padding_top and padding_bottom range settings, but they default to 0. A section root carries both .section and its own padding, so a non-zero default double-spaces the page: the condition guide shipped at 90–98px between sections against the site's 50px until this was fixed. Reach for padding only when you want space inside a section, not between them.

{ "type": "range", "id": "padding_top", "label": "Padding top",
  "min": 0, "max": 120, "step": 4, "unit": "px", "default": 0 }

The one exception is a tinted band. When use_secondary_background is on, the background would otherwise hug the text, so those sections floor their padding at 32px regardless of the setting:

padding:
  {%- if section.settings.use_secondary_background -%}
    {{ section.settings.padding_top | at_least: 32 }}px 0 {{ section.settings.padding_bottom | at_least: 32 }}px
  {%- else -%}
    {{ section.settings.padding_top }}px 0 {{ section.settings.padding_bottom }}px
  {%- endif -%};

Heading scale

All six are weight 700, letter-spacing normal. The classes are visual only — pick the element from the document outline and the class from here. The scale is responsive, so the sizes below are read from the page you are looking at rather than written down: resize the window and they change.

The quick brown fox .h1
The quick brown fox .h2
The quick brown fox .h3
The quick brown fox .h4
The quick brown fox .h5
The quick brown fox .h6

Section titles across the whole site are .h3 (21px), inside a .section__header with margin-bottom: 15px. snippets/section-header.liquid matches that — it renders an <h2> element with the .h3 class, so the outline stays correct while the size agrees with the rest of the storefront. It used .h2 (23px) before, which read as a mistake rather than a decision.

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.

ClassMeaning
1/2--tabletHalf width from the tablet breakpoint up
1/3--lap-and-upOne third from laptop up
1/4--lapOne quarter at the laptop breakpoint
1/5--deskOne fifth at desktop
1/6--wideOne sixth on wide screens

Combine them to change per breakpoint — they are additive, not exclusive.

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.

SectionBlocksUse it for
page-herometaPage h1 plus label/value pairs and a CTA
spec-tablerowStructured facts as a label/value table
grade-detailgradePaired cards with criteria and front/back photos
scale-comparisonsegmentComparing scales on one aligned track
grade-crosswalkmappingEquivalence table across systems
checklist-stepsstepNumbered walkthrough where order matters
rule-listruleSet of short titled rules
pre-order-herometa, trustPre-order page h1, release facts, CTA and reassurance row
release-countdownRelease date plus a live countdown
product-lineupgroupThe few products a launch page is actually selling
release-timelineOrdered dates with passed/upcoming states; steps with no date are skipped
rarity-breakdownProportional bar plus counts and percentages, from plain "Label: count" lines
set-overviewLong-form copy with a <details> read-more, for indexable text
_blocks@theme, @appUpstream, from Warehouse 7.0.1. A container for Theme Blocks and app blocks on any page. With no blocks/ directory in this theme, only @app blocks resolve today
tcg-newsBlog-driven news rail with per-game tag filter. Reads a native blog on purpose: Liquid cannot fetch external feeds, so ingestion happens upstream (Flow/backend) and the articles stay crawlable on our domain
release-calendarUpcoming releases across all games, from the shop's pre_order.release_calendar collection list, dated by each collection's release_date. Undated collections are skipped by design
page-navitemJump list for a long page. Targets the id section-header generates from each heading
glossarytermTerm and definition pairs as a real <dl>, with a synonyms line
worked-exampleOne real item photographed front and back, beside the reasoning. Also works as a titled callout with no photos
styleguideThis page

Every heading rendered through section-header is linkable. The snippet emits an id handleized from the heading, so “The TierZero Grade mapping” is reachable at #the-tierzero-grade-mapping with no per-section setting. page-nav targets exactly those ids.

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.

2026-08 components — how to use each one

Added in PRs #28–#33. Each entry says where the data comes from and the exact steps to put the component on a page, because "it exists in the theme" and "a merchant can use it" are different claims.

Collection list — grid mode

Any Collection list section can now render as a grid instead of the horizontal scroller: Layout → Grid, then columns for desktop (2–9) and mobile (2–4). Tablet is capped at 4 automatically. From 5 desktop columns the tile titles drop 2px so rows stay aligned. An optional release status pill reads each collection's pre_order.release_date.

Theme editor → section "Collection list" → Layout: Grid → Columns (desktop): 9, Columns (mobile): 3

Live example — real collections, grid mode, 4 columns with the status pill on:

Why grid mode kills the carousel: from 1000px up, theme.js turns every .collection-list into a Flickity carousel via watchCSS. Grid instances opt out by overriding the :after hook — scoped per section, so scroller instances elsewhere keep their carousel. Verify grids at ≥1000px after JS settles.

Running banner — Live metric block

Shows a live figure ("1,204,551 cards sold this year") from shop.metafields.tierzero.<key>. The theme never computes the number — a scheduled job writes the metafield; the block renders it verbatim and renders nothing if the metafield is empty (never "0"). In the theme editor an unset metric shows a bracketed hint instead.

1. Job writes shop.metafields.tierzero.cards_sold_2026 (any text, pre-formatted)
2. Theme editor → Announcement banner → Add block → Live metric
3. Key: cards_sold_2026 · Text: {value} cards sold this year

Live state:shop.metafields.tierzero.cards_sold_2026 is empty, so a metric block renders nothing at all on the storefront. That is the intended fail-safe — a dead job must never show “0 cards sold”.

TCG news

Blog-driven news rail. Reads a native blog on purpose — Liquid cannot fetch external feeds; ingestion happens upstream (person or Flow/backend job) and stories stay crawlable on our domain. One blog serves every game via article tags (yugioh, mtg, pokemon, riftbound).

1. Tag articles in the News blog per game
2. Add section "TCG news" → pick blog → set "Only show articles tagged": yugioh
3. On the shared tcg-game template: enable "take the tag from the collection"
   and set each collection's pre_order.news_tag metafield instead

Live example — the first two articles the section would render from the News blog:

Magnificent Monsters Update

An important update regarding Magnificent Monsters Unfortunately, we need to share an update we never expected to have…

International Express Shipping Update

International Express Shipping Update: DHL Express Returns as Our Exclusive Express Carrier At Tier Zero Games, we're constantly…

Release calendar

Cross-game upcoming releases. Source is ONE shop metafield — pre_order.release_calendar, a list of collections — and every date comes from each collection's own pre_order.release_date, so the calendar can never disagree with a set page. Rows sort by date; undated collections are skipped by design (the set page already renders "To confirm").

Adding a release (no deploy):
1. Admin → Settings → Custom data → Shop → pre_order.release_calendar → add the collection
2. Make sure the collection has pre_order.release_date
Done — every calendar instance updates.

Live example — the calendar's current contents, rendered exactly as the section renders them:

  • Chaos Origins
    Out now
  • Beyond the Brave
    Upcoming

Product card — "+N other printings"

Product cards link to the card's other printings when the product's tierzero.related_rarities_alt_arts metafield is non-empty — the same list the PDP printings selector reads, so the two can never disagree. It is a link to the PDP, never a picker: quick-add on a grid must not switch products silently. Toggle: Theme settings → Products grid → Card printings. No metafield, no link.

{%- assign other = product.metafields.tierzero.related_rarities_alt_arts.value -%}
{%- if other.size > 0 -%}
  
    +{{ other.size }} other printings
  
{%- endif -%}

Live example — how the link renders: +5 other printings

Live demo of both sections: the page.releases template (/pages/releases once the page is created in Admin).

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.

StepWhat happens
1. Resolve the collectionPage'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 factEach block has a Value from select — release_date, price, formats
3. Resolve itsnippets/pre-order-value.liquid maps the name to the metafield and outputs text, or nothing if unset.
4. Fall backMetafield → 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.

NM EX LP Muted Accent NMNear Mint
{%- 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.

StateRequirement
DefaultMeets 4.5:1 contrast for text
HoverVisible change that is not colour alone
FocusNever remove the outline without an equally visible replacement
Active / pressedReflect it in aria-pressed where it is a toggle
DisabledUse the real disabled attribute, not just styling
EmptySay what is missing and how to fix it — never render blank
ErrorMessage 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

cart search close minus check heart filter arrow-right
{%- 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.

Design tells to avoid

Named anti-patterns from hallmark, kept here because we shipped two of them ourselves and only found out by running its checklist over our own sections. See docs/core/design-review.md for how to run it.

TellWhy it reads as generatedDo instead
Side-stripe cardA 3–6px coloured border on one edge only. Very recognisable 2018-SaaS-AIHairline all round, and put the accent on the eyebrow or a small marker
Card-in-cardA bordered container holding bordered children — visual nesting with no semantic reasonPick one containment layer. Separate the inner items by ground colour
Three equal feature columnsIcon over two-line heading over three-line body, ×3, 24px gap. Every model emits itVary widths, drop one card, or use typographic rhythm instead of cards
Gradient headline or herobackground-clip: text on a purple-to-pink fill; gradient hero backgroundsSolid ink. Reach for weight, italic or scale for emphasis
Full-viewport centred heromin-height: 100vh, everything centred, one sentence, one buttonLet the hero be as tall as its content and bias it left
Pure #000 / #fffReads flat and syntheticUse the theme tokens, which are merchant-set and already tinted

The last three are largely decided by the base theme rather than by our sections, so treat them as a check on anything new rather than a licence to edit vendor files.

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.

RuleWhy
Every link has an accessible name15 image-only links shipped with empty text and alt=""
aria-expanded only on interactive elementsIt 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 aloneAdd a border, icon or text as well — WCAG 1.4.1
Respect prefers-reduced-motionKeep the state change, drop the movement
Contrast: 4.5:1 normal, 3:1 large textThe brand blue currently fails at 2.89:1