Overlay

Responsive Sheet

A side sheet that becomes a mobile drawer.

Responsive Sheet

A side sheet that becomes a mobile drawer.

Overlay

Install

Install
npx shadcn@latest add http://localhost:3000/r/responsive-sheet.json

Dependencies: button, drawer, sheet, use-is-mobile

Usage

responsive-sheet-example.tsx
"use client"

import { Button } from "@/components/ui/button"
import {
  ResponsiveSheet,
  ResponsiveSheetContent,
  ResponsiveSheetDescription,
  ResponsiveSheetHeader,
  ResponsiveSheetTitle,
  ResponsiveSheetTrigger,
} from "@/components/ui/responsive-sheet"

export function Example() {
  return (
    <ResponsiveSheet>
      <ResponsiveSheetTrigger render={<Button variant="outline" />}>Open inspector</ResponsiveSheetTrigger>
      <ResponsiveSheetContent>
        <ResponsiveSheetHeader>
          <ResponsiveSheetTitle>Inspector</ResponsiveSheetTitle>
          <ResponsiveSheetDescription>Contextual settings in a responsive surface.</ResponsiveSheetDescription>
        </ResponsiveSheetHeader>
      </ResponsiveSheetContent>
    </ResponsiveSheet>
  )
}

API

ResponsiveSheetResponsiveSheetCloseResponsiveSheetContentResponsiveSheetDescriptionResponsiveSheetFooterResponsiveSheetHeaderResponsiveSheetTitleResponsiveSheetTrigger
TypeScript
type ResponsiveSheetContextValue = { isMobile: boolean }

function ResponsiveSheet(props: React.ComponentProps<typeof Sheet>)

function ResponsiveSheetTrigger(props: React.ComponentProps<typeof SheetTrigger>)

function ResponsiveSheetClose(props: React.ComponentProps<typeof SheetClose>)

function ResponsiveSheetContent(props: React.ComponentProps<typeof SheetContent> & {
  showCloseButton?: boolean
})

function ResponsiveSheetHeader(props: React.ComponentProps<"div">)

function ResponsiveSheetFooter(props: React.ComponentProps<"div">)

function ResponsiveSheetTitle(props: React.ComponentProps<typeof SheetTitle>)

function ResponsiveSheetDescription(props: React.ComponentProps<typeof SheetDescription>)
Source code +
components/ui/responsive-sheet.tsx
"use client"

import * as React from "react"
import { XIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
import { useIsMobile } from "@/components/ui/use-is-mobile"
import { cn } from "cn"

type ResponsiveSheetContextValue = { isMobile: boolean }
const ResponsiveSheetContext = React.createContext<ResponsiveSheetContextValue | null>(null)

function useResponsiveSheet() {
  const context = React.useContext(ResponsiveSheetContext)
  if (!context) throw new Error("Responsive sheet parts must be used inside ResponsiveSheet.")
  return context
}

function ResponsiveSheet({ children, ...props }: React.ComponentProps<typeof Sheet>) {
  const isMobile = useIsMobile()
  const context = React.useMemo(() => ({ isMobile }), [isMobile])

  return (
    <ResponsiveSheetContext.Provider value={context}>
      {isMobile ? (
        <Drawer showSwipeHandle {...(props as React.ComponentProps<typeof Drawer>)}>
          {children}
        </Drawer>
      ) : (
        <Sheet {...props}>{children}</Sheet>
      )}
    </ResponsiveSheetContext.Provider>
  )
}

function ResponsiveSheetTrigger(props: React.ComponentProps<typeof SheetTrigger>) {
  const { isMobile } = useResponsiveSheet()
  return isMobile ? <DrawerTrigger {...(props as React.ComponentProps<typeof DrawerTrigger>)} /> : <SheetTrigger {...props} />
}

function ResponsiveSheetClose(props: React.ComponentProps<typeof SheetClose>) {
  const { isMobile } = useResponsiveSheet()
  return isMobile ? <DrawerClose {...(props as React.ComponentProps<typeof DrawerClose>)} /> : <SheetClose {...props} />
}

function ResponsiveSheetContent({
  className,
  children,
  side = "right",
  showCloseButton = true,
  ...props
}: React.ComponentProps<typeof SheetContent> & {
  showCloseButton?: boolean
}) {
  const { isMobile } = useResponsiveSheet()

  if (isMobile) {
    return (
      <DrawerContent className={cn("max-h-[90dvh]", className)} {...(props as React.ComponentProps<typeof DrawerContent>)}>
        {children}
        {showCloseButton && (
          <DrawerClose
            render={
              <Button
                variant="ghost"
                className="absolute top-3 right-3 bg-secondary"
                size="icon-sm"
              />
            }
          >
            <XIcon />
            <span className="sr-only">Close</span>
          </DrawerClose>
        )}
      </DrawerContent>
    )
  }

  return (
    <SheetContent
      className={className}
      side={side}
      showCloseButton={showCloseButton}
      {...props}
    >
      {children}
    </SheetContent>
  )
}

function ResponsiveSheetHeader(props: React.ComponentProps<"div">) {
  const { isMobile } = useResponsiveSheet()
  return isMobile ? <DrawerHeader {...props} /> : <SheetHeader {...props} />
}

function ResponsiveSheetFooter(props: React.ComponentProps<"div">) {
  const { isMobile } = useResponsiveSheet()
  return isMobile ? <DrawerFooter {...props} /> : <SheetFooter {...props} />
}

function ResponsiveSheetTitle(props: React.ComponentProps<typeof SheetTitle>) {
  const { isMobile } = useResponsiveSheet()
  return isMobile ? <DrawerTitle {...props} /> : <SheetTitle {...props} />
}

function ResponsiveSheetDescription(props: React.ComponentProps<typeof SheetDescription>) {
  const { isMobile } = useResponsiveSheet()
  return isMobile ? <DrawerDescription {...props} /> : <SheetDescription {...props} />
}

export {
  ResponsiveSheet,
  ResponsiveSheetClose,
  ResponsiveSheetContent,
  ResponsiveSheetDescription,
  ResponsiveSheetFooter,
  ResponsiveSheetHeader,
  ResponsiveSheetTitle,
  ResponsiveSheetTrigger,
}

Registry JSON ↗