Announcements
Deploy the Storefront Next Cartridge for Page Designer
SEO and Metadata
AEO and GEO
UI Styling
Images
Static Assets
CLI Reference
B2C Commerce Release Notes
Ask the Community
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.
| Dependency | Version | Purpose |
|---|---|---|
| tailwindcss | 4.x.x+ | Utility-first CSS framework |
| @radix-ui/* | Various | Headless UI primitives |
| class-variance-authority | 0.7.1 | Component variant management |
| clsx | 2.1.1 | Conditional class composition |
| tailwind-merge | 3.4.0 | Tailwind class conflict resolution |
cn() utility for conditional or combined class names: import { cn } from '@/lib/utils'. Example: cn('rounded p-4', isActive && 'ring-2').sm:, md:, lg:, xl:, 2xl:.style={{ ... }}) for styling..module.css) or separate CSS files for component-level styles.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:
bg-background, bg-muted, bg-cardtext-foreground, text-muted-foreground, text-primaryborder-borderbg-primary, text-primary-foreground, hover:bg-primary/90Avoid 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/.
1@import "tailwindcss";
2@import "tw-animate-css";
3
4@theme inline {
5 --radius-sm: calc(var(--radius) - 4px);
6 --radius-md: calc(var(--radius) - 2px);
7 --radius-lg: var(--radius);
8 --color-background: var(--background);
9 --color-foreground: var(--foreground);
10 --color-primary: var(--primary);
11 --color-primary-foreground: var(--primary-foreground);
12 /* ... more semantic tokens */
13}
14
15:root {
16 --radius: 0.625rem;
17 --background: #ffffff;
18 --foreground: #3f3f46;
19 --primary: #2563eb;
20 --primary-foreground: #ffffff;
21 --secondary: #f3f4f6;
22 --muted: #f9fafb;
23 --accent: #dbeafe;
24 --destructive: #b91c1c;
25 --border: #fafafa;
26 --ring: #3b82f6;
27 /* ... more tokens */
28}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:
1npx shadcn@latest add <component-name>This ejects the component into src/components/ui/ with the right dependencies and styles.
src/components/ui/.src/components/ui/; keep that directory for ejected shadcn components only.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.
class-variance-authority (cva).cn() utility (clsx + tailwind-merge).A helper function that combines clsx for conditional classes with tailwind-merge to resolve conflicting utilities.
1// src/lib/utils.ts
2import { clsx, type ClassValue } from 'clsx';
3import { twMerge } from 'tailwind-merge';
4
5export function cn(...inputs: ClassValue[]) {
6 return twMerge(clsx(inputs));
7}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.
1// src/components/ui/button.tsx (simplified example)
2import { cn } from "@/lib/utils";
3
4type ButtonVariant = "default" | "outline";
5type ButtonSize = "default" | "sm";
6
7const variantClasses: Record<ButtonVariant, string> = {
8 default: "bg-primary text-primary-foreground hover:bg-primary/90",
9 outline: "border bg-background hover:bg-accent hover:text-accent-foreground",
10};
11
12const sizeClasses: Record<ButtonSize, string> = {
13 default: "h-9 px-4 py-2",
14 sm: "h-8 px-3 text-xs",
15};
16
17function Button({
18 variant = "default",
19 size = "default",
20 className,
21 ...props
22}: React.ComponentProps<"button"> & {
23 variant?: ButtonVariant;
24 size?: ButtonSize;
25}) {
26 return (
27 <button
28 className={cn(
29 "inline-flex items-center justify-center rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50",
30 variantClasses[variant],
31 sizeClasses[size],
32 className,
33 )}
34 {...props}
35 />
36 );
37}Multi-part components are organized as separate functions:
1// src/components/ui/card.tsx
2function Card({ className, ...props }: React.ComponentProps<"div">) {
3 return (
4 <div
5 className={cn(
6 "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
7 className,
8 )}
9 {...props}
10 />
11 );
12}
13
14function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
15 return (
16 <div
17 className={cn(
18 "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
19 className,
20 )}
21 {...props}
22 />
23 );
24}Feature components apply Tailwind classes directly.
1<Card
2 className={cn(
3 "group border rounded-xl overflow-hidden w-full min-w-0 max-w-full",
4 "flex flex-col-reverse h-full shadow-sm gap-0 py-0",
5 "transition-all duration-200 hover:shadow-md",
6 className,
7 )}
8>
9 {/* ... */}
10</Card>Tailwind uses prefix-based responsive modifiers.
1<div className="px-4 sm:px-6 lg:px-8 py-8">
2 <div className="grid md:grid-cols-2 gap-6">{/* Content */}</div>
3 <div className="flex flex-col sm:flex-row gap-4 justify-center">{/* Buttons */}</div>
4</div>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.
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.
1/* src/theme/tokens/core.css */
2--icon-star: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M11.525%202.295a.53.53%200%200%201%20.95%200l2.31%204.679a2.123%202.123%200%200%200%201.595%201.16l5.166.756a.53.53%200%200%201%20.294.904l-3.736%203.638a2.123%202.123%200%200%200-.611%201.878l.882%205.14a.53.53%200%200%201-.771.56l-4.618-2.428a2.122%202.122%200%200%200-1.973%200L6.28%2021.28a.53.53%200%200%201-.77-.56l.881-5.139a2.122%202.122%200%200%200-.611-1.879L2.045%209.865a.53.53%200%200%201%20.294-.904l5.166-.755a2.122%202.122%200%200%200%201.597-1.16z%22%2F%3E%3C%2Fsvg%3E");
3--icon-check: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M20%206%209%2017l-5-5%22%2F%3E%3C%2Fsvg%3E");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.
1/* src/theme/base.css */
2[data-slot="section-title"] {
3 display: inline-flex;
4 align-items: center;
5 gap: 0.5rem;
6}
7
8[data-slot="section-title"]::before {
9 content: "";
10 display: inline-block;
11 flex-shrink: 0;
12 width: 1rem;
13 height: 1rem;
14 background-color: currentColor;
15 -webkit-mask: var(--icon-star) center / contain no-repeat;
16 mask: var(--icon-star) center / contain no-repeat;
17}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.
1[data-slot="section-title"][data-complete]::before {
2 -webkit-mask: var(--icon-check) center / contain no-repeat;
3 mask: var(--icon-check) center / contain no-repeat;
4}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.
Note
<button>, <nav>, <main>, etc.) and appropriate ARIA where needed.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:
Use a CSS component class (@layer components in src/theme/base.css) only when:
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.
1/* src/theme/base.css — CSS component class */
2@layer components {
3 .section-container {
4 @apply px-4 sm:px-8 lg:px-16 max-w-screen-2xl mx-auto;
5 }
6}1/* React component — has structure, props, and children */
2function CategoryBanner({ title, image }: CategoryBannerProps) {
3 return (
4 <div className="section-container">
5 <img src={image} alt="" />
6 <h1>{title}</h1>
7 </div>
8 );
9}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.
Note
Quick reference:
| Do | Don’t |
|---|---|
| Tailwind utility classes | Inline styles, CSS modules, component-level .css files |
cn() for conditional classes | Manual 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 |