ResponsiveModal

Adaptive component that renders as a modal on desktop and drawer on mobile

Installation

pnpm add @wandercom/design-system-web

ResponsiveModal combines Modal (Radix Dialog) and Drawer (Base UI) to provide responsive behavior.

Usage

The ResponsiveModal component automatically adapts between Modal and Drawer based on viewport size, providing an optimal experience across devices.

import { useState } from 'react';
import { ResponsiveModal } from '@wandercom/design-system-web/ui/responsive-modal';
import { Button } from '@wandercom/design-system-web/ui/button';

export function Example() {
  const [open, setOpen] = useState(false);

  return (
    <ResponsiveModal
      open={open}
      onOpenChange={setOpen}
      trigger={<Button>Open</Button>}
      title="Edit profile"
      description="Make changes to your profile here. Click save when you're done."
      footer={
        <>
          <Button variant="outline">Cancel</Button>
          <Button variant="primary">Save changes</Button>
        </>
      }
    >
      <div>Form content here</div>
    </ResponsiveModal>
  );
}

Examples

Loading example...

The recommended approach is to control the open state programmatically. This ensures the state persists correctly when switching between modal and drawer as the viewport changes:

import { useState } from 'react';
import { ResponsiveModal } from '@wandercom/design-system-web/ui/responsive-modal';

export function ControlledExample() {
  const [open, setOpen] = useState(false);

  return (
    <ResponsiveModal
      open={open}
      onOpenChange={setOpen}
      trigger={<button>Open</button>}
      title="Responsive content"
    >
      <p>This appears as a modal on desktop and drawer on mobile</p>
      <button onClick={() => setOpen(false)}>Close</button>
    </ResponsiveModal>
  );
}

Responsive behavior

The ResponsiveModal automatically adapts to the viewport size:

  • Desktop (≥744px): Renders as a centered modal dialog
  • Mobile (<744px): Renders as a bottom drawer

This provides an optimal experience across devices without requiring manual detection or separate implementations.

Smooth resize transitions

When the viewport is resized while the ResponsiveModal is open, the component intelligently handles the transition between modal and drawer modes by temporarily disabling animations. This prevents jarring visual glitches and ensures a smooth user experience during window resizing. Once resizing completes, normal animations are restored.

Slots pattern

Use the slots prop to customize styles for specific parts:

<ResponsiveModal
  trigger={<button>Open</button>}
  title="Custom styles"
  slots={{
    content: "max-w-2xl",
    title: "text-2xl",
    description: "text-muted"
  }}
>
  Content here
</ResponsiveModal>

When to use

Use ResponsiveModal when you need a single implementation that works well across all devices:

  • Form submissions and data entry
  • Confirmation dialogs
  • Settings and preferences
  • Content that requires user interaction
  • Any modal experience that should adapt to device size

If you need explicit control over desktop or mobile behavior, use the Modal or Drawer components directly.

Props

ResponsiveModal

trigger?

React.ReactNode
Element that triggers the modal or drawer to open.

title?

React.ReactNode
Modal or drawer title content.

description?

React.ReactNode
Modal or drawer description content.

primaryAction?

{ label: string; action: () => void }
Primary action button configuration. Automatically rendered in footer unless footer prop is provided.

secondaryAction?

{ label: string; action: () => void }
Secondary action button configuration. Automatically rendered in footer unless footer prop is provided.

variant?

'default' | 'destructive'
Visual variant affecting primary button style. Defaults to default.

size?

'default' | 'wide'
Size variant for modal on desktop. Only applies to modal view, not drawer. Defaults to default.

formId?

string
Form ID to associate with the primary action button, converting it to a submit button.

children?

React.ReactNode
Main modal or drawer content.

open?

boolean
Controlled open state of the modal or drawer.

onOpenChange?

(open: boolean) => void
Event handler called when the open state changes.

defaultOpen?

boolean
Default open state for uncontrolled usage.

onOpenAutoFocus?

(event: Event) => void
Event handler called when the modal or drawer is opened and focus is automatically set.

onCloseAutoFocus?

(event: Event) => void
Event handler called when the modal or drawer is closed and focus is automatically returned.

slots?

ModalSlots | DrawerSlots
Slot-based className overrides for subcomponents. Available slots: trigger, overlay, content, header, footer, title, description.

className?

string
Additional CSS classes for the content container.

Breakpoint

The responsive breakpoint is set at 744px:

  • Desktop: min-width: 744px → Modal (centered dialog)
  • Mobile: max-width: 743px → Drawer (bottom sheet)

This breakpoint is defined using the useMediaQuery hook and cannot be customized per instance. For custom breakpoint behavior, use the Modal and Drawer components directly with your own media query logic.

Accessibility

The ResponsiveModal component includes proper accessibility features across both modal and drawer modes:

  • Automatically manages focus when opened and closed
  • Traps focus within the content when open
  • Closes on escape key press
  • Supports click outside to close
  • Uses aria-labelledby to link title to content
  • Uses aria-describedby to link description to content
  • Returns focus to trigger element when closed
  • Provides appropriate animations for visual feedback
ResponsiveModal