# Theme architecture

How Techno's files are organized, where CSS and JavaScript live, and how to work on the theme locally.

Techno is an Online Store 2.0 theme built on Shopify's [Skeleton theme](https://github.com/Shopify/skeleton-theme). It has no build step: the files in the theme are the files the browser gets. Liquid renders every page on the server, and JavaScript adds interactive behavior on top.

> **Warning:** Editing theme files means your changes won't carry over when you update Techno. Duplicate your theme first, and prefer the options in [Custom code](custom-code.md).

## Directory structure

```
techno/
├── assets/
│   ├── critical.css        shared styles, loaded on every page
│   └── techno.js           shared theme JavaScript, loaded deferred; components used on some pages keep theirs in {% javascript %} tags
├── blocks/                 theme blocks, shared between sections; a _ prefix marks a private block
├── config/
│   ├── settings_schema.json
│   └── settings_data.json  the three presets: Techno, Flagship and Datasheet
├── layout/
│   ├── theme.liquid        every page
│   └── password.liquid     the password page, without header or footer
├── listings/               each preset's own templates and header and footer groups, installed with that preset
│   ├── techno/templates/   index.json, the same as templates/index.json
│   ├── flagship/           templates/ (index, product) and sections/ (header and footer groups)
│   └── datasheet/          templates/ (index, product, collection, search, list-collections) and sections/
├── locales/
│   ├── en.default.json          storefront text
│   └── en.default.schema.json   theme editor labels
├── sections/               sections, _blocks.liquid, header-group.json and footer-group.json
├── snippets/               shared partials
└── templates/              JSON templates and gift_card.liquid
```

### Blocks and the `_blocks` section

The Product, Featured product, Article, Rich text and Footer sections accept `@theme`, so they offer every public theme block, including blocks merchants generate in the theme editor. A block whose file name starts with `_`, such as `blocks/_buy_buttons.liquid`, is private: only the sections that list it by name offer it. Techno makes the blocks that read `closest.product` or `closest.article`, and the footer column blocks, private, so each appears only where it works. The six public blocks are Eyebrow, Heading, Text, Button, Collapsible row and Custom Liquid.

`sections/_blocks.liquid` is the section Shopify wraps a block in when a merchant generates it in a new section. It accepts `@theme` and `@app`, isn't offered under **Add section**, and can't be added to the Header group. Its styles follow Rich text's spacing.

A section's `{% stylesheet %}` only ships on pages where that section renders. The footer column classes and `.tc-featured-product__title` are defined in their sections and used by private blocks that only those sections list, so they're always present when used. If you make one of those blocks public, move its rules into the block or `assets/critical.css`.

## Layout

`layout/theme.liquid` renders, in order:

1. `snippets/css-variables.liquid`: resolves the ten color settings and every size setting into CSS custom properties on `:root`. See [Design tokens](design-tokens.md).
2. `assets/critical.css`, preloaded.
3. `assets/techno.js`, deferred.
4. `snippets/standard-events.liquid`: a module script that loads Shopify's standard events library and dispatches `shopify:page:view` once the document is ready. `layout/password.liquid` and `templates/gift_card.liquid` render it too. See [JavaScript components › Standard events and actions](javascript.md#standard-events-and-actions).
5. The script that configures `Shopify.actions.updateCart` and `openCart` with Techno's handlers, above `{{ content_for_header }}` so its configuration is the one that counts.
6. A skip link, the optional scanline overlay, the Header group, `<main id="MainContent">`, the Footer group, the back-to-top button (`snippets/back-to-top.liquid`, while its setting is on), the Cart drawer section, the quick view dialog (`snippets/quick-view-drawer.liquid`, while Theme settings › Product cards › "Show quick view button" or "Show quick add button" is on), and `snippets/card-swatches.liquid` with no card id while "Show swatches" is on, which outputs nothing but makes the swatch script run on every page.
7. A visually hidden live region, `#tc-live-region`, that announces cart, filter, search and variant updates to screen readers when no drawer is open. Each drawer renders its own copy through `snippets/drawer-live-region.liquid` — see [Accessibility conventions](#accessibility-conventions).

## CSS

| Where | What goes there |
| --- | --- |
| `assets/critical.css` | Shared primitives: page layout, type scale, panels, buttons, form controls, grids, drawers and the buy box |
| `{% stylesheet %}` in each section, block and snippet | Styles that belong to that one component |

Shopify collects the `{% stylesheet %}` tags into one stylesheet per page, holding only the CSS of the sections, blocks and snippets that page renders, so a component's styles are included once however many times it's used. Liquid isn't rendered inside these tags. HTML fetched later with the [Section Rendering API](https://shopify.dev/docs/api/ajax/section-rendering) brings its section's CSS in a `<style data-section-stylesheet>` element: code that inserts part of a response has to insert that element too, as the quick view, the cart sections and product recommendations do (see [JavaScript components › Section Rendering](javascript.md#section-rendering)), or the fetched markup can arrive without its styles.

Rules to follow:

* **No color values outside `css-variables.liquid`.** Every color reads a `--tc-*` variable, so all four color palettes and merchant overrides keep working.
* **Don't lower the control border tokens.** `--tc-border-control` and `--tc-border-control-accent` are the 3:1 non-text contrast floor across all four palettes.
* **Scale type with `--tc-type-scale`.** Write sizes as `calc(14px * var(--tc-type-scale))` so "Base text size" applies.
* **Motion must respect `prefers-reduced-motion`.** `critical.css` already stops animations when it's set.

## JavaScript

`assets/techno.js` holds the theme's shared JavaScript, with no dependencies. Components used on only some pages keep their scripts in their own `{% javascript %}` tags. The header section keeps the sticky header's `<tc-sticky-header>` and the menu's `<tc-header-menu>` in its own `{% javascript %}` tag, `sections/announcement-bar.liquid` keeps the countdown's `<tc-countdown>` in its, `sections/quick-order-list.liquid` keeps the quick order list's `<tc-quick-order>`, `sections/slideshow.liquid` keeps `<tc-slideshow>`, `blocks/_buy_buttons.liquid` keeps the sticky add-to-cart bar's `<tc-sticky-buy>`, `snippets/back-to-top.liquid` keeps `<tc-back-to-top>`, `snippets/product-media.liquid` keeps the gallery swipe and the full-screen viewer's `<tc-lightbox>` (the quick view renders it with `script_only: true` so the swipe runs on pages with no gallery of their own), `snippets/quick-view-drawer.liquid` keeps the quick view's `<tc-quick-view>`, and `snippets/card-swatches.liquid` keeps the card swatches' `<tc-card-swatches>`; Shopify bundles section and block scripts, and snippet scripts, into deferred files of their own, loaded after `techno.js`. Each file's script runs only on pages that render that file. All of them define custom elements that upgrade server-rendered HTML, so forms, links and `<details>` elements work before, and without, the script. See [JavaScript components](javascript.md).

## Maintainer notes

Theme Store rules don't allow minified code, so every comment in a shipped file is downloaded by shoppers. Long explanations live outside the shipped files instead:

| Shipped file | Long notes | Anchor left in the file |
| --- | --- | --- |
| `assets/critical.css` | `docs/critical-css.md` | `/* why: docs/critical-css.md#<slug> */` |
| `assets/techno.js` | `docs/techno-js.md` | `// why: docs/techno-js.md#<slug>` |
| `{% stylesheet %}` in sections and snippets | A `{%- comment -%}` block before the tag | `/* see note: <selector> */` |
| `{% javascript %}` in sections, blocks and snippets | A `{%- comment -%}` block before the tag | `/* see note: <name> */` |

The `docs/` folder is in the source repository and isn't part of the theme you download from Shopify.

## Local development

With [Shopify CLI](https://shopify.dev/docs/api/shopify-cli) installed:

```bash
# Preview on a development store, with hot reload
shopify theme dev --store your-store.myshopify.com

# Lint Liquid, JSON and theme conventions
shopify theme check

# Upload as an unpublished theme
shopify theme push --unpublished
```

`.shopifyignore` keeps repository files such as `README.md`, `docs/` and `documentation/` out of CLI uploads.

## Accessibility conventions

* Every form input has a unique `id` and a `<label for>`.
* Drawers are native `<dialog>` elements opened with `showModal()`, so focus is trapped and Escape closes them. Closing a drawer returns focus to the element that had it when the drawer opened. If the theme re-rendered that element in the meantime, for example after a variant change, focus goes to the element that now has the same `id`.
* Dropdown menus, mega menu panels and filters are `<details>` elements that open without JavaScript. Escape and outside clicks close them. A header menu panel also closes when keyboard focus leaves it, and with Header › "Open menus on hover" on it opens when a mouse pointer rests on it.
* Cart, filter and search changes are announced through a live region: `#tc-live-region` in the layout when no drawer is open, and the drawer's own region when one is. `showModal()` makes everything outside the open `<dialog>` inert, so a message written to the layout's region while a drawer is up is never announced. Every drawer carries a region, including the menu drawer, because a cart or filter response can land while it is open. Variant changes are announced through `#tc-live-region` only when no drawer is open. While one is, the page behind it is inert, and a sentence about it would overwrite the drawer's own message, so it is dropped. A variant picker inside the open drawer, as in the quick view, announces through that drawer's region.
* Custom code that announces something writes to the reachable region itself, because `assets/techno.js` keeps its own announcer private (see [JavaScript components](javascript.md)). Find the region with `document.querySelector('dialog.tc-drawer[open] [data-live-region]') || document.getElementById('tc-live-region')`, which is the open drawer's region when a drawer is open and the layout's otherwise, and set its `textContent` to the message. Skip an empty message, which would only clear what the region holds.
* Custom code should also follow the theme's timing. If a drawer has only just opened, or you can't tell how long it has been open, wait about 100 ms before writing: the drawer's region enters the accessibility tree as the drawer opens, and a message written at that moment isn't announced. The theme waits the same 100 ms before it announces an add to cart. Don't announce a change to the page behind an open drawer. The shopper can't reach that page, and the message would replace the drawer's own, which is why the theme drops variant messages while a drawer is open.
* Buttons and controls use a 44 px minimum touch target (`--tc-tap-min`).
* Machine identifiers such as SKUs, barcodes and URLs carry `dir="ltr"` so they read correctly on right-to-left pages.

## See also

* [Design tokens](design-tokens.md): the CSS variables
* [JavaScript components](javascript.md): custom elements and data attributes
* [Custom code](custom-code.md): changes that survive updates
