shadcn/ui'da ekranın kenarından kayarak açılan paneller için iki bileşen vardır: Sheet, Dialog bileşenini genişleterek sayfanın sağından, solundan, üstünden ya da altından açılan bir yan panel çizer; Drawer ise Vaul kütüphanesine dayanan ve özellikle mobilde parmakla sürüklenerek kapatılabilen bir çekmece bileşenidir. Kurulum için npx shadcn@latest add sheet ve npx shadcn@latest add drawer komutları kullanılır. Masaüstünde ayar paneli, filtre ya da mobil menü için Sheet; mobilde alttan açılan bir eylem paneli için Drawer daha doğal bir seçimdir.

Bu yazıda iki bileşenin parçalarını, Sheet'in side prop'unu ve boyutlandırmasını, Drawer'ın yönlerini ve belgedeki en kullanışlı örnek olan responsive dialog kalıbını, yani geniş ekranda Dialog, dar ekranda Drawer gösteren bileşeni ele alıyoruz. Ortadan açılan pencereler için Dialog ve Alert Dialog yazısına, serinin tamamı için shadcn/ui öğrenme rehberi sayfasına bakabilirsiniz.

Sheet: Dialog'un kenara yaslanmış hali

shadcn/ui: Sheet sayfası bileşeni, ekranın ana içeriğini tamamlayan içerikleri göstermek için Dialog'u genişleten bir bileşen olarak tanımlar. Bu tanım kaynak kodda da görülür: Sheet, Dialog ile aynı paketi, yani @radix-ui/react-dialog paketini kullanır. Sonuç olarak odak yönetimi, karartma katmanı ve kapanma davranışı Dialog ile aynı temele dayanır; değişen şey içeriğin konumu ve animasyonudur.

bash
npx shadcn@latest add sheet

Elle kurulumda @radix-ui/react-dialog paketini yükleyip kaynak kodu components/ui/sheet.tsx dosyasına kopyalarsınız. Dosya şu parçaları dışa aktarır: Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle ve SheetDescription. Dialog'dan farklı olarak overlay ve portal parçaları ayrıca dışa aktarılmaz; SheetContent bunları kendi içinde kullanır.

Temel kullanım

typescript
import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function FiltrePaneli() {
  return (
    <Sheet>
      <SheetTrigger asChild>
        <Button variant="outline">Filtreler</Button>
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Ürünleri filtreleyin</SheetTitle>
          <SheetDescription>
            Kategori ve fiyat aralığını seçip listeyi daraltın.
          </SheetDescription>
        </SheetHeader>
      </SheetContent>
    </Sheet>
  )
}

Bu kod bir buton çizer; tıklandığında panel varsayılan olarak sağ kenardan kayarak açılır ve sağ üst köşesinde çarpı simgeli bir kapatma düğmesi bulunur. Kaynakta bu kapatma düğmesi her zaman çizilir; Dialog'daki gibi showCloseButton seçeneği Sheet'te yoktur. Düğmeyi kaldırmak isterseniz dosya sizin olduğu için sheet.tsx içinde ilgili satırı düzenlemeniz gerekir.

side prop'u: panel hangi kenardan açılır?

side prop'u: panel hangi kenardan açılır? Panelin iç yapısı, Mobil menü olarak Sheet ve SheetClose

Belgeye göre SheetContent bileşenine verilen side prop'u panelin hangi kenardan çıkacağını belirler ve top, right, bottom ya da left değerlerini alır. Kaynak koddaki varsayılan değer right'tır. Her değer için farklı bir sınıf grubu uygulanır:

sideKonumBoyut (kaynak koddaki varsayılan)
rightSağ kenar, tam yükseklikw-3/4, sm ve üstünde en fazla max-w-sm
leftSol kenar, tam yükseklikw-3/4, sm ve üstünde en fazla max-w-sm
topÜst kenar, tam genişlikYükseklik içeriğe göre (h-auto)
bottomAlt kenar, tam genişlikYükseklik içeriğe göre (h-auto)

Belgedeki örnek dört değerin hepsini bir döngüyle gösterir. as const ile tanımlanan dizi, TypeScript'in side değerlerini dar bir birleşim tipi olarak görmesini sağlar; böylece yanlış bir değer yazarsanız derleme sırasında yakalanır.

typescript
const SHEET_SIDES = ["top", "right", "bottom", "left"] as const

export function SheetSide() {
  return (
    <div className="grid grid-cols-2 gap-2">
      {SHEET_SIDES.map((side) => (
        <Sheet key={side}>
          <SheetTrigger asChild>
            <Button variant="outline">{side}</Button>
          </SheetTrigger>
          <SheetContent side={side}>
            <SheetHeader>
              <SheetTitle>Profili düzenle</SheetTitle>
              <SheetDescription>
                Değişiklikleri yapın, bitince kaydedin.
              </SheetDescription>
            </SheetHeader>
            <SheetFooter>
              <SheetClose asChild>
                <Button type="submit">Kaydet</Button>
              </SheetClose>
            </SheetFooter>
          </SheetContent>
        </Sheet>
      ))}
    </div>
  )
}

Değişiklik günlüğünün Haziran 2023 bölümü bu prop'un tarihçesini de anlatır: eskiden position olan ad, diğer bileşenlerle uyumlu olsun diye side olarak değiştirilmiş ve ayrı size prop'ları kaldırılmıştır. Eski bir projeden kod taşıyorsanız position ya da size kullanan satırları buna göre güncellemeniz gerekir.

Panelin iç yapısı

SheetContent bir dikey esnek kutudur (flex flex-col gap-4) ve içine koyduğunuz parçaları üstten alta dizer. SheetHeader başlık ile açıklamayı p-4 boşluklu bir kutuda toplar; SheetTitle kalın, SheetDescription ise text-muted-foreground rengiyle küçük yazılır. SheetFooter bileşeninin sınıfları arasında mt-auto bulunur; bu sayede alt alan, içerik kısa olsa bile tam yükseklikteki yan panellerde panelin en altına itilir. Kaydet ve vazgeç düğmelerini footer'a koymanız, uzun formlarda bile düğmelerin her zaman aynı yerde durmasını sağlar. Karartma katmanı Dialog'daki ile aynıdır (bg-black/50) ve açılış ile kapanışta solarak görünür.

Animasyon süreleri de kaynakta ayrı ayrı tanımlıdır: panel açılırken duration-500, kapanırken duration-300 sınıfı uygulanır ve her kenar için ilgili slide-in-from-* ile slide-out-to-* sınıfları seçilir. Panelin daha hızlı açılmasını istiyorsanız aramanız gereken yer bu sınıflardır. Bu tür ufak düzenlemeleri bileşen dosyasında yapmak shadcn/ui'ın açık kod yaklaşımının olağan bir parçasıdır; varyant ekleme gibi daha yapısal değişiklikler için cva ile bileşen özelleştirme yazısına bakabilirsiniz.

Mobil menü olarak Sheet ve SheetClose

Sheet'in en yaygın kullanımlarından biri küçük ekranlarda gezinme menüsüdür: side="left" ile soldan açılan bir panelin içine bağlantı listesi konur. Burada sık gözden kaçan nokta, bir bağlantıya tıklandığında panelin kendiliğinden kapanmamasıdır; tek sayfalı uygulamalarda sayfa değişse bile panel açık kalabilir. Belgedeki örnek, kaydet düğmesini SheetClose asChild ile sararak tıklamada paneli kapatır. Aynı yöntemi bağlantılara da uygulayabilirsiniz: her bağlantıyı SheetClose asChild ile sardığınızda tıklama hem gezinmeyi yapar hem de paneli kapatır. Diğer yol, paneli open ve onOpenChange ile kontrol edip gezinmeden sonra durumu false yapmaktır.

Sheet boyutunu değiştirmek

Sheet boyutunu değiştirmek

Boyut için ayrı bir prop olmadığından genişlik ya da yükseklik CSS sınıflarıyla verilir. Belgedeki örnek, sağdan açılan panelin genişliğini küçük ekranlarda 400 piksel, sm ve üstünde 540 piksel yapar:

typescript
<SheetContent className="w-[400px] sm:w-[540px]">
  <SheetHeader>
    <SheetTitle>Ayarlar</SheetTitle>
    <SheetDescription>Bildirim tercihlerinizi düzenleyin.</SheetDescription>
  </SheetHeader>
</SheetContent>

Burada dikkat edilecek bir ayrıntı var: kaynak koddaki sağ ve sol paneller sm:max-w-sm sınıfıyla bir üst sınır taşır. Daha geniş bir panel istiyorsanız yalnızca genişlik vermek yetmeyebilir; üst sınırı da kendi sınıfınızla ezmeniz gerekir. Sınıfların nasıl birleştiği ve çakışan Tailwind sınıflarında hangisinin kazandığı cn() fonksiyonu yazısında anlatılıyor. SheetHeader ve SheetFooter kendi iç boşluklarını (p-4) taşır; panel içeriğine eklediğiniz form alanlarına da benzer bir yatay boşluk vermeyi unutmayın.

Drawer: Vaul tabanlı çekmece

shadcn/ui: Drawer sayfası bileşenin Vaul kütüphanesi üzerine kurulduğunu yazar. Değişiklik günlüğünde Drawer, Aralık 2023 sürümüyle eklenen bileşenler arasında yer alır ve özellikle mobilde açılması önerilir. Kurulumda vaul paketi yüklenir; depodaki kayıt tanımı bağımlılık olarak @radix-ui/react-dialog paketini de listeler.

bash
npx shadcn@latest add drawer
npm install vaul

İlk satır CLI ile kurulumdur, ikinci satır elle kurulumdaki bağımlılık adımıdır. Dosya Drawer, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, DrawerPortal ve DrawerOverlay parçalarını dışa aktarır. Belgedeki kullanım örneği bir başlık, açıklama ve iki düğmeli bir alt alandan oluşur:

typescript
<Drawer>
  <DrawerTrigger asChild>
    <Button variant="outline">Aç</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Emin misiniz?</DrawerTitle>
      <DrawerDescription>Bu işlem geri alınamaz.</DrawerDescription>
    </DrawerHeader>
    <DrawerFooter>
      <Button>Gönder</Button>
      <DrawerClose asChild>
        <Button variant="outline">Vazgeç</Button>
      </DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Belgedeki kısa kullanım örneğinde DrawerClose içine asChild olmadan bir Button konur; yukarıda, Dialog örneklerindeki ve responsive örnekteki yazımla aynı olsun diye asChild ekledik. Böylece iç içe iki düğme oluşmaz.

Drawer'ın iç parçaları Sheet ile neredeyse aynı sınıfları kullanır: DrawerFooter da mt-auto ile alta itilir ve düğmeleri dikey olarak dizer, DrawerTitle kalın yazılır, karartma katmanı yine bg-black/50 rengindedir. Bu benzerlik bilinçlidir; aynı içeriği iki bileşen arasında taşırken görünüm büyük ölçüde korunur. Aşağıdaki responsive örnek de tam olarak bu benzerlikten yararlanır.

Drawer'ın yönleri ve görünümü

Kaynak koda bakınca DrawerContent bileşeninin yönünü Vaul'un yazdığı data-vaul-drawer-direction özniteliğinden okuduğunu görürsünüz. Alt ve üst yönde panel en fazla ekran yüksekliğinin yüzde 80'i (max-h-[80vh]) kadar uzar ve dış köşeleri yuvarlatılır; sağ ve sol yönde ise Sheet'e benzer biçimde w-3/4 ve sm:max-w-sm genişliği kullanılır. Alttan açılan çekmecede üst kısımda küçük gri bir tutamaç çubuğu görünür; bu çubuk yalnızca bottom yönünde gösterilir. DrawerHeader da alt ve üst yönlerde metni ortalar, md ve üstünde sola yaslar.

Yönü nasıl seçeceğiniz ve sürükleme davranışının ayrıntıları Vaul'un kendi API'sine aittir; shadcn/ui belgesi bu konularda Vaul belgeleri sayfasına yönlendirir. Buradaki yazının kapsamı shadcn/ui'ın eklediği görünüm katmanıdır.

Responsive dialog: masaüstünde Dialog, mobilde Drawer

Drawer sayfasındaki en öğretici örnek, Dialog ve Drawer bileşenlerini birleştirerek ekran genişliğine göre farklı bileşen gösteren bir pencere kurar. Masaüstünde ortada açılan bir Dialog, mobilde alttan açılan bir Drawer görünür; içerik olarak ise iki tarafta da aynı form bileşeni kullanılır. Açık durumu tek bir open değişkeninde tutulur ve iki bileşene de open ile onOpenChange olarak verilir.

typescript
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import { useMediaQuery } from "@/hooks/use-media-query"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export function DrawerDialogDemo() {
  const [open, setOpen] = React.useState(false)
  const isDesktop = useMediaQuery("(min-width: 768px)")

  if (isDesktop) {
    return (
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogTrigger asChild>
          <Button variant="outline">Profili düzenle</Button>
        </DialogTrigger>
        <DialogContent className="sm:max-w-[425px]">
          <DialogHeader>
            <DialogTitle>Profili düzenle</DialogTitle>
            <DialogDescription>Değişiklikleri yapıp kaydedin.</DialogDescription>
          </DialogHeader>
          <ProfileForm />
        </DialogContent>
      </Dialog>
    )
  }

  return (
    <Drawer open={open} onOpenChange={setOpen}>
      <DrawerTrigger asChild>
        <Button variant="outline">Profili düzenle</Button>
      </DrawerTrigger>
      <DrawerContent>
        <DrawerHeader className="text-left">
          <DrawerTitle>Profili düzenle</DrawerTitle>
          <DrawerDescription>Değişiklikleri yapıp kaydedin.</DrawerDescription>
        </DrawerHeader>
        <ProfileForm className="px-4" />
        <DrawerFooter className="pt-2">
          <DrawerClose asChild>
            <Button variant="outline">Vazgeç</Button>
          </DrawerClose>
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}

function ProfileForm({ className }: React.ComponentProps<"form">) {
  return (
    <form className={cn("grid items-start gap-6", className)}>
      <div className="grid gap-3">
        <Label htmlFor="email">E-posta</Label>
        <Input type="email" id="email" defaultValue="ornek@alanadi.com" />
      </div>
      <div className="grid gap-3">
        <Label htmlFor="username">Kullanıcı adı</Label>
        <Input id="username" defaultValue="@ornek" />
      </div>
      <Button type="submit">Kaydet</Button>
    </form>
  )
}

Örneğin dayandığı useMediaQuery kancası bir shadcn/ui bileşeni değildir; belge sitesinin kendi kodunda, depodaki apps/v4/hooks/use-media-query.tsx dosyasında durur ve add drawer komutuyla projenize gelmez. Bu yüzden örneği kullanmak için kancayı kendiniz yazmanız gerekir. Depodaki sürüm kısadır: bir medya sorgusunu matchMedia ile dinler ve eşleşme değiştikçe durumu günceller.

typescript
// hooks/use-media-query.tsx
import * as React from "react"

export function useMediaQuery(query: string) {
  const [value, setValue] = React.useState(false)

  React.useEffect(() => {
    function onChange(event: MediaQueryListEvent) {
      setValue(event.matches)
    }

    const result = matchMedia(query)
    result.addEventListener("change", onChange)
    setValue(result.matches)

    return () => result.removeEventListener("change", onChange)
  }, [query])

  return value
}

Kanca ilk render'da false döndürür ve gerçek değeri efekt çalıştıktan sonra öğrenir. Yani geniş ekranda bile bileşen bir an için mobil dalı, yani Drawer tetikleyicisini oluşturur, ardından Dialog dalına geçer. İki dalda da tetikleyici aynı görünümde olduğu için bu geçiş genellikle fark edilmez, ama tetikleyicileri farklı tasarlarsanız bu ayrıntıyı hesaba katın. Kancanın @/hooks yolundan içe aktarılabilmesi için projenizdeki yol takma adının bu klasörü kapsaması gerekir; takma adlar components.json ayarları yazısında anlatılıyor.

Sheet, Drawer ve Dialog karşılaştırması

Sheet, Drawer ve Dialog karşılaştırması: Dayandığı kütüphane, Konum, Köşe kapatma düğmesi, Boyut
ÖzellikSheetDrawerDialog
Dayandığı kütüphane@radix-ui/react-dialogvaul@radix-ui/react-dialog
Konumtop, right, bottom, left (varsayılan right)Vaul yönüne göre; tutamaç yalnızca alttan açılıştaEkranın ortası
Köşe kapatma düğmesiHer zaman varYok; DrawerClose ile eklenirVar, showCloseButton ile kapatılabilir
BoyutclassName ile (eski size prop kaldırıldı)Alt ve üstte en fazla 80vhclassName ile, varsayılan sm:max-w-lg
Tipik kullanımYan menü, filtre, ayar paneliMobil eylem paneliForm, onay, ayrıntı

Uygulamanızın ana gezinmesi için kenar paneli arıyorsanız ayrıca Sidebar bileşeni yazısına bakın; Sidebar kendi daralma ve mobil davranışını getirir ve Sheet'ten daha kapsamlı bir yerleşim bileşenidir.

Sık yapılan hatalar

  • Eski prop adlarını kullanmak: Sheet'te position yerine side kullanılır, size prop'u ise kaldırılmıştır. Boyut için className verin.
  • Geniş panelde üst sınırı unutmak: sağ ve sol paneller sm:max-w-sm taşır; yalnızca w-[540px] yazmak beklediğiniz genişliği vermiyorsa üst sınırı da ezin.
  • useMediaQuery kancasını hazır sanmak: responsive örnekteki kanca CLI ile gelmez; dosyayı kendiniz oluşturmalısınız.
  • İki bileşene ayrı durum vermek: responsive örnekte Dialog ve Drawer aynı open değişkenini paylaşır. Ayrı durum tutarsanız pencere açıkken ekran genişliği değiştiğinde iki taraf birbirini tutmaz.
  • Başlığı atlamak: SheetTitle ve DrawerTitle panelin ne olduğunu anlatır; görsel olarak istemiyorsanız sr-only ile gizleyin, silmeyin.
  • Drawer içeriğine yatay boşluk vermemek: örnekte form className="px-4" alır; aksi halde alanlar ekran kenarına yapışır.

Sık Sorulan Sorular

Sheet ile Dialog arasındaki fark nedir?

İkisi de aynı Radix Dialog paketine dayanır. Belge Sheet'i, ana içeriği tamamlayan içerik için Dialog'u genişleten bir bileşen olarak tanımlar. Pratikteki fark konumdur: Dialog ortada açılır, Sheet ekranın bir kenarından kayar.

Drawer masaüstünde de kullanılabilir mi?

Bileşen her ekranda çalışır, ama belge onu özellikle mobil için öne çıkarır ve masaüstü için Dialog ile birleştirilmiş responsive bir örnek sunar. Hangi davranışı istediğinize göre ikisinden birini ya da bu birleşik kalıbı seçebilirsiniz.

Sheet'i kodla nasıl kapatırım?

Sheet, Dialog'un kök bileşenini kullandığı için open ve onOpenChange prop'larını kabul eder. Durumu kendiniz tutup örneğin bir bağlantıya tıklandığında false yapabilirsiniz. Bu kalıp Dialog yazısında ayrıntılı olarak gösteriliyor.

Mobil gezinme menüsü için Sheet mi Drawer mı kullanmalıyım?

Belge bu seçim için bir kural koymaz. Kenardan açılan bir bağlantı listesi istiyorsanız side="left" ile bir Sheet, alttan açılan ve sürüklenerek kapanan bir panel istiyorsanız Drawer kullanabilirsiniz. Uygulamanın kalıcı kenar menüsü içinse Sidebar bileşeni daha uygun bir başlangıç noktasıdır.

Drawer'ı yukarıdan açabilir miyim?

shadcn/ui kaynağı top, bottom, left ve right yönleri için stil içerir. Yönün nasıl seçildiği Vaul'un API'sine aittir; belgede bağlantısı verilen Vaul sayfasından kontrol edin.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: shadcn/ui öğrenme rehberi · Dialog ve Alert Dialog · Sidebar kurulumu · Popover, Tooltip ve Hover Card · Field bileşeni · cn() fonksiyonu