UI Styling

UI styling shapes the storefront’s look and feel, including layout, spacing, colors, typography, and interactive states. It provides a consistent, accessible, and responsive shopping experience for your storefront across devices.

Storefront Next uses Tailwind CSS (v4) for utility-first styling and shadcn/ui patterns, and Radix UI for headless accessible components.

Here are the Tailwind-related dependencies in this package.

Verify dependency versions against the template package.json before each release to ensure version numbers are current.

DependencyVersionPurpose
tailwindcss4.x.x+Utility-first CSS framework
@radix-ui/*VariousHeadless UI primitives
class-variance-authority0.7.1Component variant management
clsx2.1.1Conditional class composition
tailwind-merge3.4.0Tailwind class conflict resolution
  • Use Tailwind utility classes in component JSX for layout, spacing, typography, and colors.
  • Use the cn() utility for conditional or combined class names: import { cn } from '@/lib/utils'. Example: cn('rounded p-4', isActive && 'ring-2').
  • Follow mobile-first responsive patterns using breakpoint prefixes: sm:, md:, lg:, xl:, 2xl:.
  • Do not use inline styles (style={{ ... }}) for styling.
  • Do not use CSS modules (.module.css) or separate CSS files for component-level styles.
  • Global and theme styles belong in src/theme/ only. The entry point is src/theme/index.css, with tokens split across src/theme/tokens/, base resets in src/theme/base.css, and component overrides in src/theme/overrides/.

Colors and theme values are defined as CSS variables (design tokens). Use semantic token-based classes instead of hard-coded colors:

  • Backgrounds: bg-background, bg-muted, bg-card
  • Text: text-foreground, text-muted-foreground, text-primary
  • Borders: border-border
  • Interactive: bg-primary, text-primary-foreground, hover:bg-primary/90

Avoid raw color utilities (e.g. bg-[#hex]) so the app stays consistent with the theme.

The global and theme styles are in src/theme/ only. The entry point is src/theme/index.css, with tokens split across src/theme/tokens/, base resets in src/theme/base.css, and component overrides in src/theme/overrides/.

Presentational UI components are built on Radix UI primitives with shadcn/ui as the styling layer. They live in src/components/ui/.

Add new components only via the official CLI so they are ejected with the correct config and Tailwind setup:

This ejects the component into src/components/ui/ with the right dependencies and styles.

  • Do add and customize shadcn components by editing the files in src/components/ui/.
  • Do not create custom components inside src/components/ui/; keep that directory for ejected shadcn components only.
  • Do not manually copy components from the shadcn docs; always use the CLI so configuration (e.g. components.json) stays in sync.

Keeping src/components/ui/ limited to ejected shadcn components makes upgrades and maintenance predictable. For custom UI, use src/components/ (or another feature directory) and compose or wrap shadcn components as needed.

Components are stored in src/components/ui/ and follow the shadcn/ui pattern.

  • Built on Radix UI primitives for accessibility.
  • Styled with Tailwind utility classes.
  • Variants managed with class-variance-authority (cva).
  • Classes composed using cn() utility (clsx + tailwind-merge).

A helper function that combines clsx for conditional classes with tailwind-merge to resolve conflicting utilities.

Style patterns for components include variant-based components with CVA, compound components, and direct utility class usage.

With CVA, you can create variants of an existing component by extending its Tailwind classes without overriding them. Here’s a minimal example.

Multi-part components are organized as separate functions:

Feature components apply Tailwind classes directly.

Tailwind uses prefix-based responsive modifiers.

Hardcoded Tailwind color utilities like bg-red and text-green are blocked via an ESLint rule. Use semantic tokens (for example, bg-primary, text-foreground) or CSS variable classes instead.

  • Radix UI: Use Radix primitives for accessible behavior (focus, keyboard, ARIA).
  • Icons: Use Lucide React and React Simple Icons for iconography.

When you want a purely decorative icon in front of (or after) an element whose component you shouldn’t fork — a shared title, a label rendered deep in a shadcn primitive — add it with a ::before or ::after pseudo-element in theme CSS instead of editing the JSX. This keeps component-level styling out of components and lets you hook a stable data-slot or structural selector rather than threading a prop through.

The tokens, selectors, and icons below are examples only. Adapt them to your own storefront. The file paths (src/theme/tokens/core.css, and src/theme/base.css) are where global tokens and base rules live.

Use mask + background-color, not content: url(...). A masked SVG is tintable: background-color: currentColor paints the icon in the element’s text color, so it tracks light/dark and theme changes automatically. A content: url(...) image renders at its baked-in colors and can’t inherit currentColor.

Define the SVG once as a token so it’s reusable and themeable. Percent-encode the SVG so characters like #, %, <, >, and quotes survive the data URI intact — an unencoded # or % truncates the URI and the mask silently fails to load. Inside a mask, the SVG’s alpha channel is what matters; the stroke color is never painted, so use a literal black as the stroke value (not currentColor, which doesn’t resolve inside a mask). The visible color comes from background-color on the pseudo-element.

Apply the icon using ::before or ::after in src/theme/base.css. Set content: "" to activate the pseudo-element, then apply the mask and let background-color: currentColor tint it to match the surrounding text color.

Swap icons on state by toggling only the mask. Have the component set a data-* attribute on an ancestor when state changes, then add a rule that overrides just the mask image — the size and tint stay in place.

Keep the decorated structure stable. Decoration hooked to structural selectors (data-slot, :first-child, > span) breaks if that structure shifts. Keep anchored elements mounted — render them empty rather than conditionally removing them — so the icon doesn’t detach. If you decorate by position, don’t conditionally add or remove the siblings around the decorated element.

  • Use semantic HTML (<button>, <nav>, <main>, etc.) and appropriate ARIA where needed.
  • Ensure keyboard navigation and visible focus states for interactive elements.
  • Aim for WCAG compliance (contrast, focus order, labels).
  • Keep spacing and typography consistent with the design system defined in src/theme/ and Tailwind config.

Tailwind’s utility-first approach means most styling lives inline in JSX. Before extracting a reusable abstraction, read the official guide on managing reuse — it covers multi-cursor editing, loops, and component extraction as the preferred strategies before reaching for CSS abstractions.

Use a React component (the default choice) when:

  • The pattern involves markup structure—multiple elements, slots, children
  • There is logic, state, or event handling
  • It accepts props that change behavior or content
  • It composes other components (shadcn, Radix, etc.)

Use a CSS component class (@layer components in src/theme/base.css) only when:

  • The pattern is pure layout/styling—padding, max-width, centering, typography presets
  • There is no logic, state, or props—just a bag of CSS properties
  • It needs to be applied to many different HTML elements across the codebase (divs, sections, wrappers)
  • Utilities need to override it in specific contexts (the components layer is lower specificity than utilities)

Example: section-container—consolidates px-4 sm:px-8 lg:px-16 max-w-screen-2xl mx-auto into one class, used by 30+ files. A page can add max-w-4xl alongside it and the utility wins.

Rule of thumb: if you can express it as a single className string with no JSX children, it’s a CSS class. If it renders elements or accepts props, it’s a React component.

Don’t use @utility for multi-property compositions that need to be overridable. The utility layer has the highest specificity, so any override attempt (e.g., adding max-w-4xl alongside a @utility class) would lose. Use @layer components instead.

Quick reference:

DoDon’t
Tailwind utility classesInline styles, CSS modules, component-level .css files
cn() for conditional classesManual string concatenation for className
Design tokens (bg-background, text-muted-foreground)Hard-coded colors
npx shadcn@latest add <name>Manually copying or creating components in src/components/ui/
Global/theme styles in src/theme/Scattered or duplicate global CSS