Building sections

Styling

Tailwind v4, colour schemes, transitions, fonts.

Tailwind v4. Theme settings become CSS variables, which become Tailwind tokens:

merchant picks a colour --theme-background --color-backgroundbg-background

So bg-background and text-foreground follow whatever colour scheme the section is set to, with no rebuild when a merchant changes colours.

Colour schemes

Four ship by default; merchants can edit or add more. Every section has a picker, and <body> uses the theme default. Each scheme emits a .color-scheme-n class that sets those variables.

To add a role:

  1. Add a color to the definition array in settings_schema.json
  2. Map it in role
  3. Emit a --theme-* variable in css-variables.liquid
  4. Add a matching token to @theme in main.css

The role object needs all ten keysbackground, text, primary_button, secondary_button, primary_button_border, secondary_button_border, on_primary_button, on_secondary_button, links, icons. A partial object is rejected.

Transitions

Anything that appears and disappears gets one. Use CSS plus usePresence:

1const { mounted, visible } = usePresence(open, 300)
2if (!mounted) return null
3
4<div className={`transition-opacity duration-300 ease-out motion-reduce:transition-none ${
5 visible ? 'opacity-100' : 'opacity-0'
6}`} />

if (!open) return null makes exit animations impossible — the element is gone before anything can run. usePresence splits mounted (in the DOM) from visible (in its open state) and holds the mount long enough to animate out.

Three rules:

  • the duration must match the CSS
  • add motion-reduce:transition-none
  • set pointerEvents: 'none' while animating out, so the fading element doesn't eat clicks

Currently 300ms for drawers, 150ms for dropdowns.

Icons

Lucide. Import by name, never the namespace:

1import { ShoppingBag } from 'lucide-react' // yes
2import * as Icons from 'lucide-react' // no — pulls in all of them

Icons inherit currentColor, so they follow the colour scheme for free. Mark them aria-hidden and put the accessible name on the button or link.

Fonts

Three sources under Typography: Shopify's library, your own files, or an external service. Custom font fields accept either a theme asset filename (brand.woff2, dropped in src/fonts/) or a full URL from Content → Files.

Shopify has no file-upload setting type — image_picker and video filter by media type and won't offer a font. A text field is the only option, which is why it accepts both.

Motion

usePrefersReducedMotion() tracks the visitor's setting live, rather than reading it once — the OS setting can be toggled while the page is open. Anything that moves on its own should check it. Carousels do this for you.