#6509·heroui

[BUG] - Toast: `useMediaQuery` for action button position causes SSR hydration mismatch

Author: aosmcleodCreated May 11, 2026Updated May 11, 2026

HeroUI Version

3.0.4

Describe the bug

In packages/react/src/components/toast/toast.tsx, the ToastProvider uses useMediaQuery to conditionally render the action button in different positions based on viewport width:

typescript
const isMobile = useMediaQuery("(max-width: 768px)");
// ...
// Inside getDefaultChildren:
{isMobile && actionProps?.children ? (
  <ToastActionButton ... />  // renders INSIDE ToastContent on mobile
) : null}
// ...
{!isMobile && actionProps?.children ? (
  <ToastActionButton ... />  // renders OUTSIDE ToastContent on desktop
) : null}

During SSR (or any server-rendered environment like Next.js App Router), useMediaQuery returns false because window.matchMedia is not available. On a mobile client, the first client render returns true. This creates a hydration mismatch — the server HTML has the action button outside ToastContent, but the client expects it inside.

Root cause

useMediaQuery is not SSR-safe when used to conditionally change the structure of the React tree (different DOM positions). It's safe for styling/classes but not for conditional rendering that changes element hierarchy.

Suggested fix

Option A: Always render both positions, use CSS to show/hide:

typescript
<ToastContent>
  {!!title && <ToastTitle>{title}</ToastTitle>}
  {!!description && <ToastDescription>{description}</ToastDescription>}
  {actionProps?.children ? (
    <ToastActionButton className="sm:hidden" {...actionProps}>{actionProps.children}</ToastActionButton>
  ) : null}
</ToastContent>
{actionProps?.children ? (
  <ToastActionButton className="hidden sm:flex" {...actionProps}>{actionProps.children}</ToastActionButton>
) : null}

Option B: Use a useEffect-based approach that defaults to the server-rendered layout and only switches on the client after hydration:

typescript
const [isMobile, setIsMobile] = useState(false); // always false on server
useEffect(() => {
  setIsMobile(window.matchMedia("(max-width: 768px)").matches);
}, []);

Option A is preferred as it avoids layout shift.

Impact

  • SSR: React hydration mismatch warning in Next.js / Remix / any SSR framework
  • Visual: Potential layout flash on mobile as the action button moves positions after hydration
  • Severity: Affects all SSR apps using Toast.Provider with action buttons on mobile viewports

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

  1. Use Next.js App Router with <Toast.Provider /> in the root layout
  2. Trigger a toast with actionProps (e.g., an "Undo" button)
  3. View the page on a mobile viewport (< 768px)
  4. Check the browser console for hydration mismatch warnings
  5. Observe the action button briefly appearing in the wrong position before settling

Expected behavior

Toast layout should be consistent between server and client renders, with no hydration mismatch.

Operating System Version

macOS

Browser

Chrome (mobile viewport emulation)