Zephora UI

Configuration

Global settings for every Zephora component — wrap your app once with ZephoraConfigProvider and configure locale, overlay z-index layers, the portal container and the headless default.

Setup

import { ZephoraConfigProvider } from "@zephora/react";

export default function App() {
  return (
    <ZephoraConfigProvider
      locale="tr"
      zIndex={{ modal: 2000, toast: 2100 }}
      appendTo={typeof document !== "undefined" ? document.body : null}
      unstyled={false}
    >
      <YourApp />
    </ZephoraConfigProvider>
  );
}

ZephoraConfigProvider props

PropTypeDefaultDescription
localestring"en"Active locale for component UI strings. Only "en" is embedded; load others from @zephora/theme/locales/* (module or JSON) and register with addLocale().
zIndex{ dropdown?, overlay?, modal?, popover?, toast?, tooltip?: number }Overrides the layering of floating elements. Written as --z-index-* CSS variables on the root element.
appendToHTMLElement | nulldocument.bodyContainer that Portal-based overlays (Dialog, Select menus, Tooltip…) render into.
unstyledbooleanfalseMakes every component headless by default for Tailwind-first apps. A per-component unstyled prop still wins.

Reading the config

import { useZephoraConfig, useLocale } from "@zephora/react";

function MyField() {
  const config = useZephoraConfig(); // { locale, zIndex, appendTo, unstyled }
  const { t, messages, locale } = useLocale();
  return <button aria-label={t("close")} />;
}

z-index layers

Components take their stacking order from CSS variables with built-in fallbacks, so both the provider and plain CSS can retune them:

:root {
  --z-index-dropdown: 1000;
  --z-index-overlay: 1040;
  --z-index-modal: 1050;
  --z-index-popover: 1060;
  --z-index-toast: 1070;
  --z-index-tooltip: 1080;
}

Dark mode

@zephora/theme ships automatic dark-mode helpers. The useColorScheme hook resolves a scheme (with mode: "system" it follows the OS preference live) and by default applies the resolved theme globally:

import { useColorScheme } from "@zephora/theme";

function App() {
  // mode: "light" | "dark" | "system" (default "system")
  const { scheme, theme } = useColorScheme({ mode: "system" });
  return <span>Active scheme: {scheme}</span>;
}

useColorScheme(options) props

PropTypeDefaultDescription
mode"light" | "dark" | "system""system""system" follows the OS preference live; "light"/"dark" force a scheme.
light / darkThemelightTheme / darkThemeTheme objects used for each scheme.
applybooleantrueWrite the resolved theme to document.documentElement via applyTheme.
→ returns{ scheme: "light" | "dark"; theme: Theme }The resolved scheme and the theme object in effect.

The lower-level primitives are exported too:

import { getSystemColorScheme, watchSystemColorScheme } from "@zephora/theme";

// Reads the OS color scheme (SSR-safe; defaults to "light").
const scheme = getSystemColorScheme(); // "light" | "dark"

// Subscribes to OS changes. Returns an unsubscribe function.
const stop = watchSystemColorScheme((next) => console.log(next));
stop();

FOUC-free SSR

For server rendering, colorSchemeCss(light?, dark?) builds a static stylesheet that switches themes with the OS preference — no flash of the wrong theme, no JavaScript needed before paint. Setting data-zephora-scheme on <html> overrides the media query (wire it to a user toggle):

import { colorSchemeCss } from "@zephora/theme";

// Next.js app router — app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <style dangerouslySetInnerHTML={{ __html: colorSchemeCss() }} />
      </head>
      <body>{children}</body>
    </html>
  );
}

Slot classNames

Multi-slot components (Dialog, Sheet, Tabs, Card, Popover, Tooltip…) expose a per-slot class contract: the root accepts classNames typed as Partial<Record<<Name>Slot, string>> (slot-name unions like DialogSlot are exported). Slot classes are appended after the module classes and are not gated by unstyled — they work in both styled and unstyled mode; in unstyled mode they are the only classes. classNames.root merges alongside the existing className prop, which keeps targeting the root and is applied last. For compound components, classNames is set once on the root and carried to the parts via context.

Server components

The published @zephora/react bundle ships with a "use client" directive, so you can import components directly inside React Server Component trees (Next.js app router) without adding the directive yourself — every Zephora component is a client component.