Shopify Custom Fonts on Horizon: A Postmortem
A merchant migrated to Horizon and her working font disappeared. A postmortem on Shopify custom fonts and what broke between Dawn and Horizon.

Maya runs a small candle and incense studio out of Asheville. About 180 orders a month, mostly $24 single tins and $58 three-packs, almost all from Instagram and a handful of return customers. She had been on Dawn for two years with a custom serif called Brunelo on her product titles, and the font worked. She migrated to Horizon over a long weekend in May because the new card layouts fit her photos better. By Monday morning her product titles were rendering in the Horizon default sans, her variant labels had reverted to system fallback, and the cart icons in her header rendered fine. She googled "shopify custom fonts" and got a dozen Dawn-era tutorials, none of them mentioning the four things she was about to spend two evenings discovering one at a time. We see this exact stack often enough now that "shopify custom fonts on Horizon" has earned its own page in our internal notes.
This is the postmortem of what we found in Maya's store, what we got wrong on the first pass, and the four checks I now put in front of every Dawn-to-Horizon font migration.
What happened
The Brunelo font file was uploaded correctly. Maya had moved it to Settings > Files months earlier, after reading a community warning about the OTS parsing errors that the theme assets folder produces, so the CDN URL in her @font-face block was valid. We confirmed in the Network tab that the WOFF2 file was loading and parsing without errors. The browser knew about the font. Nothing on the page was using it.
Four things were stacked under that symptom, not one.
Her @font-face declaration was in a file called custom.css in her Assets folder, linked from theme.liquid with the standard {{ 'custom.css' | asset_url | stylesheet_tag }} tag right before </head>. That is the path every Dawn tutorial recommends. It is the path that has worked since the launch of Online Store 2.0. In Horizon it produces nothing. No warning, no console message, no stylesheet attached. Horizon routes stylesheet links through snippets/stylesheets.liquid, and the <head> tag in theme.liquid is silently ignored. Maya's CSS was in the file. Her tag was in theme.liquid. The browser had never been told about the stylesheet at all.
Her selectors were targeting h1, h2, .title, and .product__title. On Dawn that catches the bulk of a product page. On Horizon, typography is driven by CSS custom properties defined in snippets/theme-styles-variables.liquid: --font-body--family, --font-heading--family, --font-subheading--family, --font-accent--family. Horizon's per-block compound selectors (the .text-block.h4>* family that C_Hoff and tim_tairli documented in the August 2025 community thread) outrank an h2 rule from a custom stylesheet every time. Even if her stylesheet had loaded, the variable layer would have won.
The product title selector was a third miss. Horizon renders product titles as anchor elements nested inside .product__title, so .product__title { font-family: Brunelo; } styles a wrapper that contains no rendered text. The actual text lives in the child <a> element. It inherits font-family only if you spell out .product__title, .product__title a in the selector list.
The fourth issue surfaced when we looked at her hero. Maya had used the theme editor to apply the "H1" typography role to her hero heading, then targeted h1 in her CSS. In Horizon the typography role and the HTML element are separate settings. Her hero text was styled to look like H1, but the underlying DOM element was a <span>. Her selector matched nothing in the document. From the inspector this is easy to spot in twenty seconds. From the theme editor it is invisible.
Why it happened
Three things made this stack possible.
Dawn-era tutorials are still ranking. The first six results for any Google query about shopify custom fonts point to Dawn-specific recipes from 2022 and 2023. None of them mention CSS variables, none of them mention stylesheets.liquid, and several still reference .scss.liquid files that Shopify deprecated years ago. A merchant who searched and followed instructions in 2026 was, more often than not, applying a 2022 pattern to a 2025 architecture.
Horizon's own documentation does not surface the variable system. The theme editor exposes four typography roles but does not tell you they map to four CSS variables you can override directly. The stylesheets.liquid injection point is in the public Horizon GitHub source code; it is not in any theme editor tooltip or Shopify Help Center page I have been able to find. The community threads that document it are months old and still rank below the Dawn tutorials.
And Horizon ships at minor-version cadence. A selector that worked yesterday can stop matching after a Sunday push, which means even merchants who eventually find the variable approach get bitten again later when Horizon refactors its block scaffolding. I have a separate piece on the customization habits that break on Horizon - the font case is one specific instance of the same shape.
What we changed
The fix took longer than I would have liked. Four changes, in order:
- Moved the stylesheet link to
stylesheets.liquid. We deleted the{{ 'custom.css' | asset_url | stylesheet_tag }}tag fromtheme.liquidand added{{ 'font-overrides.css.liquid' | asset_url | stylesheet_tag: preload: true }}tosnippets/stylesheets.liquid. We renamed the file to.css.liquidso theasset_urlfilter would process. A throwawaybody { outline: 5px solid magenta; }rule we left in the file became visible immediately, which is how we confirmed the injection point was the first blocker. - Overrode the Horizon font variables. In
snippets/theme-styles-variables.liquid, inside the existing:root { }block, we added--font-heading--family: 'Brunelo', serif;and--font-body--family: 'Brunelo', serif;. After that, every component that read from the heading or body role started rendering in Brunelo without us touching individual selectors. - Updated the product title selector. We changed
.product__title { font-family: Brunelo; }to.product__title, .product__title a { font-family: 'Brunelo', serif; }. That one line fixed the product page titles across all 24 products. - Set the hero heading tag explicitly. In the theme editor, we opened the hero block settings and set the heading tag dropdown to H1. Maya had assumed the "H1" typography role implied the HTML tag. It does not. After we set the tag, her existing
h1rule started matching.
Total elapsed time: about 35 minutes for the fixes themselves, plus another 20 minutes of inspector-staring before we figured out the theme.liquid link was the silent first blocker. If you are stuck on a font migration and your CSS looks right but nothing applies, the first 15 minutes with a help1 expert is free, and that is usually enough to spot whether the injection point is your problem before you spend another evening on selectors.
What I would catch earlier next time
Four things I am putting in front of every Horizon font migration audit now.
- Verify the stylesheet link is in
stylesheets.liquid, nottheme.liquid, before anything else. A one-line diagnostic rule likebody { outline: 5px solid magenta; }tells you within ten seconds whether your CSS is reaching the page at all. If it is not, no font selector on earth will help you. - Read the four font role variables out loud from
theme-styles-variables.liquid. The variable names use a double-dash convention ---font-body--family, with two dashes betweenbodyandfamily- and it is easy to typo. I have spent more time than I want to admit chasing a font failure that turned out to be a single missing hyphen. - Check the heading tag dropdown on the block, not the typography role in theme settings. The role controls visual style. The dropdown controls the DOM. If your CSS targets
h1and the block is set to render asspan, your CSS will not match and the inspector will not flag it as an error. - Account for product titles being anchor elements. Until Horizon changes this, any selector that touches
.product__titleshould also include.product__title a. Two selectors, one rule, every time.
I should be honest about a fifth thing. The first time I worked a Dawn-to-Horizon font migration last year, I assumed the variable override alone would be enough and spent forty minutes looking for a typo in my @font-face block when the real issue was that my stylesheet had never been linked from stylesheets.liquid at all. I now check the link before I check the CSS. It cost me an evening to learn that, and Maya was the third merchant in two months I had seen hit the same combination.
The unsolved part
One open question I have not resolved cleanly: the right call on !important. The August 2025 thread between C_Hoff, devcoders, and tim_1 surfaces a real disagreement about whether to apply !important broadly on the variable declarations to cover Horizon's deeply nested block IDs, or to leave them off and chase failing selectors one by one. I have ended up doing the second on most jobs because broad !important declarations make future overrides harder, but I am not sure that is the right call long term. If your store has survived several Horizon minor releases and you have a view on which approach decays more gracefully, I would like to hear it.
Shopify custom fonts are one of those things where the move from Dawn to Horizon stops feeling like an upgrade and starts feeling like a separate platform, and the rendering pipeline is where the difference shows up first.
Still stuck? Talk to an expert.
Our vetted Shopify experts can fix this issue for you in a live session. $39 per session. Your first 15 minutes are free.