Skip to main content

Overview

Noteverse uses Radix UI primitives to create accessible dialog and drawer components. Both components support controlled and uncontrolled states.

Dialog Component

A modal dialog that overlays the page content.

Import

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
  DialogFooter,
  DialogClose,
} from '@/components/ui/dialog'

Basic Usage

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog'

export default function Example() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <button>Open Dialog</button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
          <DialogDescription>
            This action cannot be undone.
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  )
}

Components

Root component that manages dialog state.
<Dialog
  open={open}              // Controlled state
  onOpenChange={setOpen}   // State change handler
  defaultOpen={false}      // Initial state (uncontrolled)
>
  {/* Dialog content */}
</Dialog>

Styling

className="fixed inset-0 z-50 bg-black/80
          data-[state=open]:animate-in 
          data-[state=closed]:animate-out 
          data-[state=closed]:fade-out-0 
          data-[state=open]:fade-in-0"

Form Dialog Example

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/Input'
import { Button } from '@/components/ui/button'
import { useState } from 'react'

export default function CreateNoteDialog() {
  const [title, setTitle] = useState('')
  const [open, setOpen] = useState(false)
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    // Create note
    console.log({ title })
    setOpen(false)
    setTitle('')
  }
  
  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button>Create Note</Button>
      </DialogTrigger>
      <DialogContent className="sm:max-w-[425px]">
        <form onSubmit={handleSubmit}>
          <DialogHeader>
            <DialogTitle>Create New Note</DialogTitle>
            <DialogDescription>
              Enter a title for your new note.
            </DialogDescription>
          </DialogHeader>
          <div className="grid gap-4 py-4">
            <div className="grid gap-2">
              <label htmlFor="title">Title</label>
              <Input
                id="title"
                value={title}
                onChange={(e) => setTitle(e.target.value)}
                placeholder="Untitled Note"
              />
            </div>
          </div>
          <DialogFooter>
            <Button type="button" variant="outline" onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button type="submit">Create</Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  )
}

Drawer Component

A slide-out panel from the right side (or bottom on mobile).

Import

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer'

Basic Usage

import {
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer'

export default function Example() {
  return (
    <Drawer>
      <DrawerTrigger>Open Drawer</DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Drawer Title</DrawerTitle>
          <DrawerDescription>Drawer description</DrawerDescription>
        </DrawerHeader>
        <div className="p-4">
          {/* Drawer content */}
        </div>
      </DrawerContent>
    </Drawer>
  )
}

Components

Root component using Vaul library.
<Drawer
  open={open}
  onOpenChange={setOpen}
  shouldScaleBackground={true}  // Scales page content
>
  {/* Drawer content */}
</Drawer>

Responsive Behavior

The drawer adapts to screen size:
// Desktop: Right side panel (400px wide)
className="fixed inset-y-0 right-0 z-50 m-2 
          flex h-auto flex-col rounded-[10px] 
          border bg-background w-[400px]"

// Mobile: Bottom sheet (full width)
className="max-sm:inset-x-0 max-sm:bottom-0 max-sm:m-0 
          max-sm:mt-24 max-sm:w-full"

Drawer Example

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/Input'

export default function UserProfileDrawer() {
  return (
    <Drawer>
      <DrawerTrigger asChild>
        <Button variant="outline">Edit Profile</Button>
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Edit Profile</DrawerTitle>
          <DrawerDescription>
            Make changes to your profile here.
          </DrawerDescription>
        </DrawerHeader>
        <div className="p-4 space-y-4">
          <div>
            <label htmlFor="name">Name</label>
            <Input id="name" defaultValue="John Doe" />
          </div>
          <div>
            <label htmlFor="email">Email</label>
            <Input id="email" type="email" defaultValue="[email protected]" />
          </div>
        </div>
        <DrawerFooter>
          <Button>Save Changes</Button>
          <DrawerClose asChild>
            <Button variant="outline">Cancel</Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

Dialog vs Drawer

When to use each:
  • Confirmations and alerts
  • Short forms (2-3 fields)
  • Critical actions requiring focus
  • Temporary, task-focused content
  • Content that should center on screen
Example: Delete confirmation, quick note creation
  • Settings and preferences
  • Longer forms (4+ fields)
  • Navigation menus
  • Detailed information panels
  • Content that works better from the side
Example: User profile editor, detailed note preview

Accessibility

Both components are built with accessibility in mind:
  • Focus trap: Can’t tab outside the dialog/drawer
  • Focus returns to trigger when closed
  • Initial focus on first focusable element
<DialogContent>
  <Input autoFocus /> {/* Auto-focus first input */}
</DialogContent>

TypeScript Interfaces

// Dialog
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogClose = DialogPrimitive.Close

// Drawer
const Drawer = ({
  shouldScaleBackground = true,
  ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
  <DrawerPrimitive.Root
    shouldScaleBackground={shouldScaleBackground}
    {...props}
  />
)

Best Practices

  • Always provide a DialogTitle for accessibility
  • Use DialogDescription for additional context
  • Keep dialog content focused and concise
  • Use controlled state for complex workflows
  • Provide a clear way to close (button or X icon)
  • Don’t nest dialogs/drawers

Next Steps

Buttons

Learn about button components

Backgrounds

Explore animated backgrounds

Build docs developers (and LLMs) love