Salve developer guide

This page is for developers and agencies who customize Salve for a merchant. It explains how the theme is built, where things live and the conventions to follow so your changes survive theme updates. Merchant-facing setup is in the documentation hub.

Duplicate the theme before you edit code. Code changes are not covered by support, and theme updates don't carry them over. Keep a short list of the files you changed so you can reapply them.

Architecture at a glance

  • Base: built from Shopify's Skeleton theme. Online Store 2.0 with sections everywhere, header and footer section groups, theme blocks (/blocks) and static blocks for the hidden quick add section.
  • No build step, no framework. Vanilla ES modules and native CSS. Nothing is minified in the repo; Shopify minifies on delivery. No third-party requests are made by theme code.
  • Section Rendering API for every dynamic update: variant changes, cart, filters, predictive search, recommendations, pickup, quick add and product comparison all fetch server-rendered HTML (?section_id= or ?sections=) and swap it in. There is no client-side templating of product data.
  • Health data layer: snippets under snippets/health-*.liquid read product metafields (namespace from the Metafield namespace setting, default health) and feed the product blocks, product cards, cart lines and structured data. Missing data renders nothing.
  • Faster page loads: when the Faster page loads setting is on, layout/theme.liquid outputs Speculation Rules that prefetch same-origin pages with conservative eagerness (on press, not hover). Cart, checkout, account, /apps/, /services/ and any URL with a query string are excluded, as are links with rel="nofollow", target="_blank" or download. Add data-no-prefetch to any other link that must not be fetched early.
  • B2B: snippets/b2b-location.liquid renders the "Buying for" company location switcher in the header and menu drawer when customer.b2b? is true, using customer.company_available_locations and each location's url_to_set_as_current. It renders nothing for other customers.
  • Works without JavaScript: script-only controls carry the class u-js-only; their fallbacks carry u-no-js. A one-line head script swaps html.no-js for html.js.

File map

Folder What is in it
layout/ theme.liquid (head order: canonical, fonts, CSS variables, critical.css, base.css, content_for_header, module scripts, structured data) and password.liquid.
assets/ critical.css (tokens, reset, typography, core components; kept under 14 KB), base.css (the rest of the shared styles), section-*.css and snippet-*.css (styles for rarely used sections, loaded only where they render), and one ES module per component (see below).
sections/ Main templates (main-*.liquid), content sections, section groups (header-group.json, footer-group.json) and hidden fragments used by the Section Rendering API: cart-drawer, cart-icon-bubble, predictive-search, pickup-availability, quick-add, compare-data, product-card-fragment.
blocks/ Theme blocks. Product-only blocks start with an underscore (_price, _safety-information, _documents…). Public blocks (heading, text, button, image, icon-text, group, spacer, custom-liquid) can be used in any content section that accepts @theme.
snippets/ Shared markup. Every snippet starts with a {% doc %} block listing its parameters and an example call.
templates/ JSON templates (customer accounts are handled by Shopify's new customer accounts and the <shopify-account> component in the header, so there are no templates/customers/* files), including the alternates product.device, product.supplement, collection.compare, page.contact, page.about, page.documents, page.safety-notices, page.hsa-fsa and page.quick-order.
locales/ Storefront strings (en.default.json, fr, de, es, it) and editor strings (*.schema.json). No storefront text is hard-coded in Liquid.
listings/ Preset templates: listings/<preset>/templates/ holds the index, product and collection templates for Salve, Gauge and Remedy. When a merchant installs a preset, these replace the root templates of the same name. The Gauge product template uses the device layout and the Remedy one the supplement layout.

Conventions

  • CSS classes: s- sections (s-hero), b- blocks (b-price), c- shared components (c-button, c-card, c-media), u- utilities (u-page, u-visually-hidden). BEM-lite modifiers: c-button--secondary.
  • Color schemes: each scheme outputs a class color-scheme-N that defines --color-background, --color-foreground, --color-foreground-muted, --color-surface, --color-accent, --color-on-accent, --color-border, --color-notice-background, --color-notice-foreground and --color-focus as RGB triplets, so rgb(var(--color-foreground) / 0.12) works for alpha variants.
  • Tokens (set in snippets/css-variables.liquid from theme settings): spacing --space-1 … --space-32 (4 px grid), type --text-display, --text-h1 … --text-h6, --text-body, --text-small, --text-label, radii --radius-card, --radius-button, --radius-input, --radius-chip, layout --page-width, --gutter, --grid-gap, and --target-min (44 px touch targets).
  • Section CSS: put styles that a section always needs in its {% stylesheet %} tag (Shopify bundles them into one file). Styles for a section that appears on few pages go in assets/section-<name>.css, loaded with stylesheet_tag at the top of the section.
  • Inline styles are only used for CSS custom properties driven by settings, for example style="--ratio: 1" on .c-media.
  • IDs derive from section.id and block.id; never hard-code an id that could repeat on a page.
  • Accessibility: WCAG 2.2 AA. Dialogs use native <dialog> through DialogController in global.js (focus trap, Esc, return focus). Status messages go through announce(), which writes to the #a11y-status live region. Keep focus rings; never remove outline.

JavaScript

Every interactive part is a custom element that owns its listeners and re-initializes in connectedCallback, so swapped markup from the Section Rendering API just works. assets/global.js exports the shared helpers:

Export Use
publish(topic, payload), subscribe(topic, handler), EVENTS A 30-line pub/sub. Topics (all listed in Salve.EVENTS): cart:updated (payload: the cart JSON and rendered sections), variant:changed, planner:suggestion, compare:changed, header:ready ({ height }, after the header sets --header-height) and selling-plan:changed ({ planId }, empty for one-time purchase). Subscribe to cart:updated to react to any cart change. Strings for scripts come from Salve.strings; text that includes a value uses a placeholder such as [title] or [count] so each language keeps its own word order.
fetchSection(sectionId, url, params), fetchSections(ids, url) Section Rendering API helpers (HTML for one section, JSON for several).
parseHTML(html), swapInnerFrom(html, selector, target) Parse fetched markup (noscript fallbacks are dropped) and swap a region by selector.
trapFocus(), focusables(), DialogController Dialog and drawer behavior.
announce(text) Polite screen reader announcement.
debounce(), storage, prefersReducedMotion(), rootUrl() Utilities. storage.get/set wrap localStorage in try/catch; every feature works without storage.

Components and their files: <product-form> (product-form.js), <variant-picker> (variant-picker.js), <media-gallery> (media-gallery.js), <cart-drawer> and <cart-items> (cart.js), <predictive-search>, <facet-filters> (facets.js), <supply-planner>, <compare-tray> and <compare-dialog> (compare.js), <product-selector> (selector.js), <quick-order-list> (quick-order.js), <quick-add>, <pickup-availability>, <product-recommendations> (recommendations.js), <text-size-control> (text-size.js), <dismissible-notice> (notice.js), <sticky-buy-bar> (sticky-buy.js), <recently-viewed>, <shipping-estimator>, <delivery-estimate>, <back-to-top>, <slide-show> (slideshow.js), and details-disclosure.js for menus built on <details>. Scripts load as type="module" only on templates or sections that need them; an import map in the head keeps shared modules cache-busted.

Variant change contract: <variant-picker> fetches the product section with ?option_values= and swaps the price, regulatory statements, identifiers, inventory status, buy buttons, pickup, installments, quantity rules, supply planner and media selection by id, then publishes variant:changed. If you add a block that depends on the variant, give its root a stable id and add it to the swap list in variant-picker.js.

Health data snippets

Snippet Renders
health-field One field: a block setting override (dynamic source) first, then the conventional metafield. Handles rich text, multi-line and list types.
health-status Regulatory status chips or statement lines, filtered by the region setting. Only us_pma_approved ever prints "FDA approved".
health-safety The "Important safety information" band. The _buy-buttons and _price blocks render it as a fallback when a product has safety data but the merchant removed the block.
health-documents Documents grouped by type, current versions first, previous versions in a disclosure. Falls back to the simple file list metafield.
health-identifiers, health-parties, health-units, health-hsa, health-notice Identifiers, responsible parties, units per pack and price per unit, HSA/FSA eligibility, and safety notices in product, card and cart contexts.
structured-data The page's JSON-LD (snippets/structured-data.liquid, rendered from layout/theme.liquid): Organization data with the return and shipping policies from the Search engines theme settings, WebSite data with the search action, and Product data with offers, identifiers and ratings.
health-setup-checker The Health data setup panel shown only in the theme editor (request.design_mode). It lists which fields have values and refers merchants to Health data setup in the documentation; it contains no external links.

The full metafield list, types and choice values are in Health data setup. Shipped JSON never references custom metafields or metaobjects; only Liquid reads them, defensively, at render time.

Adding a section or block

  1. Copy the closest existing section. Keep color_scheme, padding_top, padding_bottom and, if it has a heading, heading_level.
  2. Put every label in locales/en.default.schema.json under t:sections.<name> and every storefront string in locales/en.default.json, then mirror the keys in the four other locale files (the theme ships a locale parity check in salve-qa).
  3. Use sentence case, American English, no ampersands, and Shopify's terminology for labels.
  4. Give the section a preset with placeholder content so it appears in the section picker and renders sensibly before the merchant adds content.
  5. If the section is interactive, make it a custom element and initialize it in connectedCallback. Listen for shopify:section:load only if the element cannot reinitialize itself.
  6. Run shopify theme check before you push. The theme ships with 0 offenses.

Quality checks the theme ships with

The salve-qa folder (not part of the theme zip) contains the checks used before every release: Theme Check, a static pass (Liquid and HTML parse, JS and CSS syntax, translation keys, upload gotchas, no-JavaScript rule, performance budgets), content gates (no claims language, no demo resources in shipped JSON), color contrast for all preset schemes, and locale parity. Budgets: critical.css under 14 KB, home page JS under 25 KB gzipped, product page JS under 45 KB gzipped.

Things not to do

  • Don't add scripts from third-party hosts inside theme files; use app blocks or the Custom Liquid section so the merchant controls them.
  • Don't output health or regulatory claims from theme code. The theme prints merchant data and fixed, neutral labels only.
  • Don't add file uploads, date of birth or symptom fields to storefront forms.
  • Don't remove the safety band fallback in _buy-buttons and _price; it is what keeps safety information visible when a block is deleted.