UI component library
The storefront's UI lives in frontend/packages/ui/ and is organized
into two tiers: a small set of base primitives and a larger set of
commerce-specific components built on top of them.
Architecture
packages/ui/src/
├── base/ # ~16 primitives (Button, Input, Select, …)
├── components/ # ~30 commerce components (ProductCard, Navigation, …)
├── helpers/ # Utility functions (cn, typography)
├── global.css # Design tokens + Tailwind theme (at package root)
└── ...
Base primitives
These are low-level building blocks with no commerce knowledge. They wrap Radix UI for accessibility and use CVA for variant management.
alert-dialog button checkbox container icon input
label radio radio-group range-slider ribbon select
skeleton switch text-area typography
Commerce components
Higher-level components that compose base primitives with business logic. Examples include product cards, cart line items, checkout summaries, and navigation.
Product display: product-card product-carousel product-gallery
product-grid product-header product-list
product-specifications product-stock-info reviews-stars
Checkout & cart: checkout-summary quantity-selector
address-card delivery-methods line-item summary
order-status-indicator shopping-list
Content & data: catalog-card catalog-carousel content-teaser
data-table table markdown-container usp-list
Navigation & layout: accordion breadcrumbs card footer
hero navigation
Modals & feedback: dialog toaster tooltip
Variant selectors: variant-selectors (button, color, and image
selector sub-components)
Component patterns
CVA variants
Components declare their visual variants with Class Variance Authority:
import { cva } from "class-variance-authority";
export const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center …",
{
variants: {
variant: {
primary: "bg-orange-500 font-bold text-black …",
secondary: "bg-black font-bold text-white …",
tertiary: "text-gray-600 hover:bg-gray-50 …",
link: "text-black hover:text-gray-500 …",
outline: "bg-white font-semibold ring-2 ring-black …",
},
size: {
lg: "gap-lg px-4 py-[10px] text-sm",
xl: "gap-sm px-4 py-2.5 text-sm md:px-6 md:py-3 md:text-base",
xxl: "gap-sm px-4 py-2.5 text-sm md:px-6 md:py-3 md:text-base",
},
},
defaultVariants: { variant: "primary", size: "xxl" },
}
);
Radix UI for accessibility
Base components use Radix UI primitives for keyboard navigation, focus
management, and ARIA attributes. The asChild pattern (via
Slot.Root) lets you swap the rendered element while keeping
behaviour:
const Component = asChild ? Slot.Root : "button";
The cn() helper
All components merge class names with cn(), which combines clsx
(conditional classes) and tailwind-merge (deduplication of Tailwind
utilities):
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
Theming
Design tokens are defined in two layers in global.css. A base layer
sets raw values as CSS custom properties, and the @theme directive
maps them into Tailwind:
/* Base layer - raw design token values */
@layer base {
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(240 10% 3.9%);
--primary: hsl(240 5.9% 10%);
--destructive: hsl(0 72.22% 50.59%);
--radius: 0.5rem;
}
}
/* Tailwind theme - maps tokens to utility classes */
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-destructive: var(--destructive);
--radius-lg: var(--radius);
}
Dark mode is enabled through a custom Tailwind variant:
@custom-variant dark (&:is(.dark *));
This means dark-mode styles activate when an ancestor has the dark
class, rather than relying on prefers-color-scheme.
Icons
The library uses Lucide React for icons. A
custom Icon component renders icons as CSS masks rather than inline
SVGs, reducing DOM size:
<span
style={{ "--mask-image": `var(--icon-${icon})` }}
className="inline-block size-6 bg-current [mask-image:var(--mask-image)]"
aria-hidden="true"
/>
Storybook
All components have Storybook stories alongside their source files. A hosted instance is available at ui.evolve.labdigital.nl. The Storybook setup includes:
- Accessibility checks via
@storybook/addon-a11y - Responsive viewport presets (minimal through extra-large)
- Auto-generated documentation from component props
Analytics
The @evolve-storefront/gtm-ecommerce package provides Google Tag
Manager Enhanced Ecommerce events. Ten standard ecommerce events are
implemented:
| Event | Trigger |
|---|---|
view_item | Product detail page view |
view_item_list | Product listing page view |
select_item | Click on a product card |
add_to_cart | Add product to cart |
remove_from_cart | Remove product from cart |
view_cart | Cart page view |
begin_checkout | Start checkout |
add_shipping_info | Select shipping method |
add_payment_info | Select payment method |
purchase | Order confirmation |
Each event sends a standard GA4 ecommerce payload with item-level
data (SKU, name, brand, category, price, quantity). The core function
sendGTMEcommerceEvent() pushes events to the dataLayer.
GTM event tracking needs to be wired per project. The package provides the event functions but the integration points in components are project-specific.