Skip to main content

Search

Search problems, hooks, and pages

Back to About

MongoDB Design System

built with: @leafygreen-ui + shadcn/ui + Tailwind

Two UI systems deliberately coexisting — and the Emotion SSR bridge required to stop one of them shipping unstyled HTML.

Why two systems

LeafyGreen is MongoDB's real design system, and using the genuine components for callouts, code blocks, and expandable cards is what makes the app look like MongoDB rather than an imitation of it. But LeafyGreen does not cover everything an app needs — sidebars, sheets, command palettes, toggles.

So shadcn/ui on Tailwind v4 handles the general application chrome, and LeafyGreen handles the surfaces where its components genuinely fit. The rule when building a new surface is to check what the surrounding code already uses and match it, rather than introducing a third pattern.

The app is dark-mode only. There is no theme toggle and no `next-themes` — a `dark` class is forced on the root `<html>` and `LeafyGreenProvider` is passed `darkMode` unconditionally, so the two systems agree on exactly one appearance instead of two.

The Emotion SSR problem

LeafyGreen styles its components with its own bundled `@emotion/css` instance. On the server that instance fills an in-memory `cache.inserted` object — and stops there. Nothing writes those styles into the streamed HTML.

The visible result is a flash of completely unstyled LeafyGreen markup: correct class names in the DOM, no CSS backing them, until hydration runs on the client and Emotion injects the styles.

The fix is a registry component that hooks `useServerInsertedHTML`, drains whatever Emotion has accumulated so far, and emits it as a `<style>` tag inside the streamed response. A `Set` of already-flushed names prevents re-emitting the same rules on later flushes of the same stream.

// src/app/emotion-registry.tsx
export default function EmotionRegistry({ children }: { children: ReactNode }) {
  const flushed = useRef<Set<string>>(null);
  if (flushed.current === null) flushed.current = new Set();

  useServerInsertedHTML(() => {
    const names = Object.keys(cache.inserted).filter((n) => !flushed.current?.has(n) && typeof cache.inserted[n] === 'string');
    if (names.length === 0) return null;
    let styles = '';
    for (const name of names) {
      flushed.current?.add(name);
      styles += cache.inserted[name];
    }
    return <style data-emotion={`${cache.key} ${names.join(' ')}`} dangerouslySetInnerHTML={{ __html: styles }} />;
  });

  return children;
}

Provider order matters

The registry has to wrap `LeafyGreenProvider`, not sit beside it. Providers compose outside-in as SerwistProvider → NuqsAdapter → EmotionRegistry → LeafyGreenProvider → NotificationProvider → Toaster.

If the registry were nested inside the LeafyGreen provider it would mount too late to capture the styles that provider and its children insert during the same server render.

// src/app/providers.tsx
<SerwistProvider swUrl="/serwist/sw.js" disable={process.env.NODE_ENV === 'development'}>
  <NuqsAdapter>
    <EmotionRegistry>
      <LeafyGreenProvider darkMode>
        <NotificationProvider>{children}</NotificationProvider>
      </LeafyGreenProvider>
      <Toaster theme="dark" richColors position="bottom-right" />
    </EmotionRegistry>
  </NuqsAdapter>
</SerwistProvider>

Important

Gotcha

Emotion styles are injected with `dangerouslySetInnerHTML`. That is the standard Emotion SSR pattern and safe here specifically because the CSS comes from LeafyGreen’s own cache — never from user input.

Where it lives in the repo

  • src/app/emotion-registry.tsx
  • src/app/providers.tsx
  • src/components/ui/
  • src/app/globals.css