Skip to main content

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:

EventTrigger
view_itemProduct detail page view
view_item_listProduct listing page view
select_itemClick on a product card
add_to_cartAdd product to cart
remove_from_cartRemove product from cart
view_cartCart page view
begin_checkoutStart checkout
add_shipping_infoSelect shipping method
add_payment_infoSelect payment method
purchaseOrder 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.

note

GTM event tracking needs to be wired per project. The package provides the event functions but the integration points in components are project-specific.