Skip to content

CSS Architecture, Naming & Logical Properties

TL;DR: Global vs. component-scoped CSS, BEM-ish naming, and logical properties for RTL support.

We write plain, native CSS only. No Sass or SCSS, no CSS-in-JS. Modern CSS already gives us everything those tools used to be needed for: custom properties, container queries, :has(), and nesting, all covered elsewhere in this section. This page covers how we organize and name what we write.

LayerLives inContains
Globalassets/base.css (or similar, loaded in layout/theme.liquid)Design tokens (custom properties), resets, typography defaults, utility classes used across many components
Component-scopedA {% stylesheet %} tag inside the section/block/snippet .liquid file itselfEverything specific to that one component: its layout, its states, anything not reused elsewhere

Shopify only loads a component’s {% stylesheet %} CSS on pages that actually render it, see CSS in Shopify for exactly how that subsetting works. This gives you the benefit of keeping styles right next to the component they belong to (developers call this “colocation”), without bloating the global file for a section only used on one template.

✅ Do❌ Don’t
Put a token used by 3+ components in the global stylesheetDuplicate the same spacing/color value across several {% stylesheet %} blocks
Put layout specific to one section inside that section’s {% stylesheet %}Add every section’s CSS to one global file “to keep it simple”
Reference global tokens (var(--space-md)) from inside scoped CSSHardcode a value in scoped CSS that already exists as a global token

We don’t require strict BEM, but we follow the same block/element/modifier shape, because it keeps class names predictable and greppable:

.testimonials { } /* block */
.testimonials__heading { } /* element */
.testimonials--compact { } /* modifier */
✅ Do❌ Don’t
.product-card__price.productCardPrice (camelCase in CSS) or .price1
.product-card--sold-out for a state variantA second, unrelated class like .grayed-out that isn’t obviously tied to .product-card
Match the class name to the component’s file name where practical (testimonials.liquid.testimonials)Give a component’s root element a generic class (.wrapper, .container) with no relation to what it is

Predictable naming also keeps CSS subsetting safe: a class name tied to its file makes it obvious, at a glance, whether that class is meant to be used elsewhere.

“Logical properties” are CSS properties like margin-inline-start. Instead of describing a fixed side, like “left” or “right,” they describe direction based on reading order, using “start” and “end.” Use a logical property everywhere a physical property has a logical equivalent, it’s what makes your layout work correctly, automatically, in right-to-left languages like Arabic or Hebrew. See Internationalization & RTL for the broader picture.

❌ Physical (breaks in RTL)✅ Logical (works in both directions)
margin-left / margin-rightmargin-inline-start / margin-inline-end
padding-left / padding-rightpadding-inline-start / padding-inline-end
left: 0 / right: 0inset-inline-start: 0 / inset-inline-end: 0
text-align: lefttext-align: start
border-leftborder-inline-start
/* ❌ WRONG — hardcodes a left-to-right assumption */
.card {
margin-left: var(--space-md);
text-align: left;
}
/* ✅ RIGHT — flips automatically under dir="rtl" */
.card {
margin-inline-start: var(--space-md);
text-align: start;
}

Test new sections with dir="rtl" in your browser’s dev tools regularly, not just right before Theme Store submission. Logical-property mistakes are cheap to catch early, but easy to miss if you never actually look. See Spacing in Liquid & CSS for the spacing-specific version of this rule, including gap over margin hacks.

✅ Do❌ Don’t
Keep component CSS in {% stylesheet %}, next to the component, unless a value is genuinely shared elsewhere. This lets you read a component’s whole behavior, markup, styling, and JS, in one file.Putting every section’s CSS in one global file. This loses the benefit of colocation, and every page ends up loading CSS it doesn’t use.
Turn any reusable value into a custom property in the global stylesheet before it gets used a second time.Hardcoding a color or spacing value that already exists as a token. It drifts out of sync the next time the token changes, because this one spot wasn’t using it.
Name classes in a BEM-ish, kebab-case shape (block__element--modifier), and tie the block name to the component’s file where practical.Using physical properties (margin-left) out of habit. This passes review fine in English, then breaks silently the first time the theme runs in a right-to-left market.
Use logical properties by default, everywhere. Treat a physical property in new CSS as something to double-check, not your default choice.Giving a component’s root element a generic class (.wrapper, .container) with no relation to what it actually is.
  • Global tokens (custom properties) live in assets/base.css. Component CSS lives in {% stylesheet %}, next to its markup.
  • Naming: kebab-case, BEM-shaped (block__element--modifier).
  • Use logical properties everywhere a physical/logical pair exists. This is what makes RTL work.