Shared utils

Cross-platform utilities, hooks, formatters, and dictionaries

Installation

Shared utils install automatically with components. To install separately:

pnpm add @wandercom/design-system-shared

Classes

cn

Class name merging with Tailwind CSS conflict resolution. Built on clsx and tailwind-merge, pre-configured with design system class groups (text sizes, shadows, etc.).

import { cn } from '@wandercom/design-system-shared/classes';

function Card({ className, elevated }) {
  return (
    <div
      className={cn(
        'rounded-lg border p-4',
        elevated && 'shadow-modal',
        className
      )}
    />
  );
}

createCn

Factory function to create a custom cn with extended Tailwind class groups. Use this when your project has custom Tailwind classes beyond the design system defaults.

import { createCn } from '@wandercom/design-system-shared/classes';

export const cn = createCn({
  extend: {
    classGroups: {
      'font-size': ['text-marketing-hero', 'text-marketing-subtitle'],
    },
  },
});

createCn executes once at module level and returns a function identical to cn but aware of your additional classes.

Hooks

Import hooks individually:

import { useMediaQuery } from '@wandercom/design-system-shared/hooks/use-media-query';

useMediaQuery

Subscribes to a CSS media query. Returns boolean | undefined (undefined during SSR).

const isWide = useMediaQuery('(min-width: 1024px)');

useIsDesktop / useIsMobile

Convenience wrappers around useMediaQuery with a 744px breakpoint.

import { useIsDesktop, useIsMobile } from '@wandercom/design-system-shared/hooks/use-media-query';

const isDesktop = useIsDesktop(); // min-width: 744px
const isMobile = useIsMobile();   // max-width: 743px

usePrefersReducedMotion

Detects the user's reduced motion preference. Returns boolean | undefined during SSR.

import { usePrefersReducedMotion } from '@wandercom/design-system-shared/hooks/use-reduced-motion';

const prefersReduced = usePrefersReducedMotion();

usePrefersReducedMotionSafe is also exported and defaults to false during SSR instead of undefined.

useStaggeredAnimation

Returns per-item CSS styles for staggered entrance animations, respecting reduced motion.

import { useStaggeredAnimation } from '@wandercom/design-system-shared/hooks/use-staggered-animation';

const { getItemStyle } = useStaggeredAnimation({
  isActive: true,
  baseDelayMs: 80,
  stepMs: 60,
});

{items.map((item, i) => (
  <div key={item.id} style={getItemStyle(i)}>{item.name}</div>
))}

useResizeObserver

Returns true while the window is actively being resized. Debounced (default 150ms).

import { useResizeObserver } from '@wandercom/design-system-shared/hooks/use-resize-observer';

const isResizing = useResizeObserver(200); // custom debounce ms

useScrolled

Returns true when the window scroll position exceeds a threshold (default 0).

import { useScrolled } from '@wandercom/design-system-shared/hooks/use-scrolled';

const isScrolled = useScrolled(50); // 50px threshold

useHeaderThemeSync

Detects when a header element overlaps dark theme sections and returns the appropriate theme. Uses intersection detection with mutation, scroll, and resize observers.

import { useHeaderThemeSync } from '@wandercom/design-system-shared/hooks/use-header-theme-sync';

const headerRef = useRef<HTMLElement>(null);
const theme = useHeaderThemeSync(headerRef, {
  enabled: true,
  selector: '[data-theme="dark"]:not([data-theme-scope="local"])',
});
// Returns "dark" | "light" | undefined

Formatters

Import formatters individually:

import { formatDateRange } from '@wandercom/design-system-shared/formatters/dates';

Dates

import {
  formatDate,
  formatDateRange,
  getShortMonthName,
  getMonthName,
  DATE_FORMATS,
} from '@wandercom/design-system-shared/formatters/dates';

formatDate(new Date(), 'MMM d, yyyy');    // "Mar 9, 2026"
formatDateRange(checkIn, checkOut);        // "Mar 15 - 20" or "Mar 15 - Apr 2"
getShortMonthName(2);                      // "Mar"
getMonthName(new Date());                  // "March"

DATE_FORMATS provides standard format strings: short, medium, long, dayOfWeek, dayOfWeekLong, monthYear, monthYearShort.

Counts

import { formatCount, formatCounts } from '@wandercom/design-system-shared/formatters/counts';

formatCount(1, { singular: 'guest' });          // "1 guest"
formatCount(3, { singular: 'guest' });          // "3 guests"
formatCount(0, { singular: 'guest', fallback: 'No guests' }); // "No guests"

formatCounts([
  { count: 2, singular: 'guest' },
  { count: 1, singular: 'pet' },
]);
// "2 guests, 1 pet"

Money

import { formatMoney } from '@wandercom/design-system-shared/formatters/money';

formatMoney({ currency: 'USD', fractional: 15000 }); // "$150.00"

The fractional value is in cents (divided by 100 for display). Uses Intl.NumberFormat for locale-aware formatting.

Formatters for search bar UI labels:

import {
  formatSearchBarDatesLabel,
  formatSearchBarGuestsLabel,
  formatFlexibleDatesLabel,
} from '@wandercom/design-system-shared/formatters/search-bar';

formatSearchBarGuestsLabel({ adults: 2, children: 1, pets: 1 });
// "3 guests, 1 pet"

formatFlexibleDatesLabel('weekend', ['jan', 'feb']);
// "Weekend in Jan, Feb"

Calendar

Shared calendar range utilities and vacation rental domain logic for consistent date selection, availability validation, and stay constraint computation across Calendar and PropertyCalendar implementations.

import {
  buildAvailabilityMap,
  getMaxCheckoutDate,
  validateCheckinDate,
  validateCheckoutDate,
} from '@wandercom/design-system-shared/calendar';

Types

AvailabilityDay — a single day's availability data from the property management system.

interface AvailabilityDay {
  date: string;           // ISO date string (YYYY-MM-DD)
  canCheckIn: boolean;
  canCheckOut: boolean;
  isBlocked?: boolean;
  minNights?: number;
  maxNights?: number;
  nightPrice?: number;
  status: 'available' | 'booked' | 'maintenance' | 'blocked';
}

DateRange — a check-in / check-out date pair, where either or both may be unset.

interface DateRange {
  start: Date | null;
  end: Date | null;
}

StayRequirements — constraints on minimum/maximum nights and allowed check-in/check-out days.

interface StayRequirements {
  minNights?: number;
  maxNights?: number;
  checkInDays?: string[];   // e.g. ["Saturday"]
  checkOutDays?: string[];
}

BookingRule — a booking rule that constrains date selection behavior.

interface BookingRule {
  type:
    | 'min-nights'
    | 'max-nights'
    | 'check-in-day'
    | 'check-out-day'
    | 'advance-booking'
    | 'gap-nights'
    | 'same-day-turnover'
    | 'quota-exhausted';
  message: string;
}

Date utilities

parseLocalDate(dateStr) — parses an ISO date string (YYYY-MM-DD) into a local Date with no timezone shift.

import { parseLocalDate } from '@wandercom/design-system-shared/calendar';

parseLocalDate('2026-06-15'); // Date for June 15, 2026, local time

getWeekdayName(date) — returns the full weekday name in US English.

getWeekdayName(new Date('2026-06-20')); // "Saturday"

buildAvailabilityMap(availability) — builds a fast-lookup Map from an availability array, keyed by ISO date string. Pass this map into validation functions instead of re-iterating the array.

import { buildAvailabilityMap } from '@wandercom/design-system-shared/calendar';

const availabilityMap = buildAvailabilityMap(availabilityDays);
const day = availabilityMap.get('2026-06-15');

Range selection

isDateInRange(date, start, end) — returns true when date falls strictly between start and end (exclusive of both endpoints).

import { isDateInRange } from '@wandercom/design-system-shared/calendar';

isDateInRange(midDate, checkIn, checkOut); // true if between

getDateRangeState(date, selected, minDate?) — derives all range state flags for a given date against the current selection. Useful for driving calendar day cell styles.

import { getDateRangeState } from '@wandercom/design-system-shared/calendar';

const {
  isRangeStart,
  isRangeEnd,
  isInRange,
  isSingleSelected,
  isRangeEndpoint,
  isDisabled,
} = getDateRangeState(date, { from: checkIn, to: checkOut }, minDate);

Availability validation

validateCheckinDate(date, availabilityMap, stayRequirements?) — validates a proposed check-in date. Returns an error string, or null when valid.

import { validateCheckinDate } from '@wandercom/design-system-shared/calendar';

const error = validateCheckinDate(date, availabilityMap, {
  checkInDays: ['Saturday'],
});
// null | "Unavailable" | "Check-in unavailable"

validateCheckoutDate(date, checkinDate, availabilityMap, stayRequirements?, rangeHasBlocked?) — validates a proposed check-out date against availability, stay requirements, and an optional range-blocked check. Returns an error string, or null when valid.

import { validateCheckoutDate } from '@wandercom/design-system-shared/calendar';

const error = validateCheckoutDate(
  checkoutDate,
  checkinDate,
  availabilityMap,
  { minNights: 3, maxNights: 14 },
);
// null | "Unavailable" | "Min 3 nights" | "Max 14 nights" | ...

getMaxCheckoutDate(checkinDate, availabilityMap, stayRequirements?) — computes the latest date a guest may check out. Returns the minimum of checkinDate + maxNights and the first blocked/booked day after check-in. Returns null when there is no computable upper bound.

import { getMaxCheckoutDate } from '@wandercom/design-system-shared/calendar';

const maxCheckout = getMaxCheckoutDate(checkinDate, availabilityMap, {
  maxNights: 14,
});

pathHasBlockedDay(availabilityMap, from, to) — returns true when the path between from and to (exclusive of both endpoints) contains a blocked, booked, or maintenance day. Use this to prevent range selections that span unavailable nights.

import { pathHasBlockedDay } from '@wandercom/design-system-shared/calendar';

if (pathHasBlockedDay(availabilityMap, checkIn, checkOut)) {
  // range crosses an unavailable night
}

Responsive variants

Type-safe utilities for building components with responsive CVA variants. Solves Tailwind's tree-shaking limitation by requiring all responsive variants defined upfront.

import {
  expandResponsiveVariants,
  type VariantPropsResponsive,
  type BreakpointValue,
} from '@wandercom/design-system-shared/responsive';

const variants = cva('', {
  variants: {
    size: { sm: '...', md: '...', lg: '...' },
    sizeMd: { sm: 'md:...', md: 'md:...', lg: 'md:...' },
  },
});

const RESPONSIVE_KEYS = ['size'] as const;
type Props = VariantPropsResponsive<typeof variants, typeof RESPONSIVE_KEYS>;

function Component(props: Props) {
  return (
    <div className={expandResponsiveVariants(variants, RESPONSIVE_KEYS, props)} />
  );
}

Enables responsive variant syntax:

<Component size={{ base: 'sm', md: 'lg' }} />

BreakpointValue<T> supports breakpoints: base, sm, md, lg, xl, 2xl, 3xl, 4xl.

Thumbhash

Progressive image loading with Thumbhash placeholders. Tiny (~20-30 byte) image previews that display while full images load.

import {
  thumbHashToDataURL,
  getThumbHashDimensions,
} from '@wandercom/design-system-shared/thumbhash';

const dataUrl = thumbHashToDataURL(thumbHashString);
const { width, height } = getThumbHashDimensions(thumbHashString);

Both functions return null on error and work in browser and SSR contexts.

Dictionaries

Toast dictionary

Pre-defined toast message templates with support for dynamic values. Provides consistent toast copy across applications.

import { createToastDictionary } from '@wandercom/design-system-shared/dictionaries/toasts';

const toasts = createToastDictionary({
  BookingSaved: {
    label: 'Booking saved',
    description: 'Your booking has been saved.',
  },
});

createToastDictionary merges your custom toasts with the shared defaults (CopiedToClipboard, ChangedTheme, ErrorGeneral, ErrorActionFailed, ErrorInvalidInput, ErrorInputRequired, RequestReceived). Labels and descriptions can be strings or functions receiving { count?, date?, value? }.

Countries

Curated country lists (ISO 3166-1 alpha-2 codes) for use with PhoneInput, CountrySelect, and other country-aware components.

import { ALL_COUNTRIES } from '@wandercom/design-system-shared/countries';
import { STRIPE_CONNECT_COUNTRIES } from '@wandercom/design-system-shared/countries';
ExportCountDescription
ALL_COUNTRIES235All ISO-2 country codes
STRIPE_PAYMENT_COUNTRIES50Stripe Tax-supported countries
STRIPE_SUPPORTED_COUNTRIES49Countries where Stripe is available for businesses
STRIPE_CONNECT_COUNTRIES46Stripe Connect countries (used by WanderOS)

Each list is a const array of lowercase ISO-2 strings. The CountryCode type extracts the union of all valid codes.

To use with PhoneInput, convert codes to CountryData[] via filterCountries:

import { STRIPE_CONNECT_COUNTRIES } from '@wandercom/design-system-shared/countries';
import { filterCountries } from '@wandercom/design-system-web/ui/phone-input';

<PhoneInput countries={filterCountries(STRIPE_CONNECT_COUNTRIES)} />

Stripe theme

Composable appearance config for Stripe Elements that mirrors DS token values — input sizing, border radii, typography, and color for both light and dark modes.

import {
  createStripeAppearance,
  createStripeGoogleFonts,
  stripeLayout,
} from '@wandercom/design-system-shared/stripe-theme';

<Elements
  stripe={stripePromise}
  options={{
    mode: 'payment',
    currency: 'usd',
    amount: 10_000,
    fonts: createStripeGoogleFonts('Instrument Sans'),
    appearance: createStripeAppearance(isDark),
  }}
>
  <PaymentElement options={{ layout: stripeLayout }} />
</Elements>
Loading example...

createStripeAppearance

Returns a complete Stripe Appearance object. Accepts either a boolean for the default Wander palette, or a StripeColors object for custom theming.

// Default palette
createStripeAppearance(isDark)

// Custom palette — override individual color values
const colors = { ...resolveStripeColors(false), colorDanger: '#c53030' };
createStripeAppearance(colors)

// Appearance-level overrides merged on top
createStripeAppearance(isDark, { labels: 'floating' })

resolveStripeColors

Returns a StripeColors object for a given mode. Use this to read resolved color values or to build a custom palette.

import {
  resolveStripeColors,
  createStripeAppearance,
  type StripeColors,
} from '@wandercom/design-system-shared/stripe-theme';

const colors: StripeColors = resolveStripeColors(isDark);

StripeColors fields:

FieldDescription
colorBackgroundPage/card background
colorPrimaryPrimary text and interactive color
colorSurfaceSecondary surface (Stripe colorSuccess)
inputBackgroundInput field background
colorPlaceholderPlaceholder text
borderSecondaryDefault border
borderHoverHover border
borderSelectedFocus/selected border
colorDangerError color

createStripeVariables / createStripeRules

Composable primitives for building partial appearance objects. Useful when you need to merge variables or rules selectively.

import {
  resolveStripeColors,
  createStripeVariables,
  createStripeRules,
} from '@wandercom/design-system-shared/stripe-theme';

const colors = resolveStripeColors(isDark);

const appearance = {
  theme: isDark ? 'night' : 'flat',
  variables: {
    ...createStripeVariables(colors),
    fontSizeBase: '14px',
  },
  rules: {
    ...createStripeRules(colors),
    '.Label': { fontSize: '14px', marginBottom: '8px' },
  },
};

stripeRadius

Radius tokens used across the Stripe appearance.

import { stripeRadius } from '@wandercom/design-system-shared/stripe-theme';

stripeRadius.input   // "8px"
stripeRadius.global  // "16px"
stripeRadius.tab     // "4rem"
stripeRadius.button  // "9999px"

stripeLayout

Pre-configured PaymentElement layout using the accordion style.

import { stripeLayout } from '@wandercom/design-system-shared/stripe-theme';

<PaymentElement options={{ layout: stripeLayout }} />

Font helpers

import {
  createStripeGoogleFonts,
  createStripeLocalFonts,
} from '@wandercom/design-system-shared/stripe-theme';

// Load from Google Fonts
createStripeGoogleFonts('Instrument Sans')

// Load a self-hosted font
createStripeLocalFonts('/fonts/InstrumentSans.woff2', 'Instrument Sans')