shadcn/ui içinde combobox, tek başına kurulan bir bileşen değildir: resmî belge onu <Popover /> ile <Command /> bileşenlerinin birleşimi olarak tanımlar. Yani aranabilir bir seçim kutusu için önce bu iki bileşeni projenize eklersiniz, sonra belgedeki örnek kodu kendi dosyanıza kopyalayıp listenizi ve durum (state) yönetiminizi uyarlarsınız. Popover açılır paneli, Command ise arama kutusu ile filtrelenen listeyi sağlar.
Bu yazıda resmî combobox örneğini satır satır açıyoruz, belgedeki dört örneği (temel, popover, dropdown menu ve responsive) karşılaştırıyoruz, mobilde Drawer kullanan sürümün gerektirdiği küçük kancayı gösteriyoruz ve sık yapılan hataları topluyoruz. shadcn/ui yaklaşımına yeniyseniz önce shadcn/ui öğrenme rehberi sayfasına göz atabilirsiniz; Command bileşeninin kendi ayrıntıları ise Command palette yazısında anlatılıyor.
Combobox neden ayrı bir bileşen değil?

Belgenin Combobox sayfasında kurulum bölümü kendi komutunu vermez; bunun yerine Popover ve Command sayfalarının kurulum adımlarına yönlendirir. Bunun nedeni yapının kendisidir: combobox, bir tetikleyici düğme, açılan bir yüzey ve bu yüzeyin içinde aranabilir bir liste demektir. shadcn/ui bu üç işi zaten yapan parçaları ayrı ayrı sunduğu için, combobox bir "tarif" olarak belgelenir. Bu da shadcn/ui felsefesine uyar: kod sizin projenize kopyalanır ve parçaları istediğiniz gibi birleştirirsiniz (ayrıntı için shadcn/ui nedir yazısı).
Depodaki kayıt dosyasına bakınca bağımlılıklar da netleşir. command bileşeni cmdk paketine ve ayrıca shadcn/ui dialog bileşenine dayanır (çünkü aynı dosyada CommandDialog da tanımlıdır). popover bileşeni ise @radix-ui/react-popover paketini kullanır. Command belgesi, bileşenin pacocoursey tarafından yazılan cmdk üzerine kurulu olduğunu belirtir.
Kurulum
CLI ile iki bileşeni tek komutta ekleyebilirsiniz. add komutu birden fazla bileşen adını argüman olarak kabul eder; seçeneklerin tamamı CLI komutları yazısında anlatılıyor. Belgeler pnpm, yarn ve bun sekmeleri de gösterir; burada npm biçimini kullanıyoruz.
npx shadcn@latest add popover commandElle kurulum tercih ederseniz Popover sayfası npm install @radix-ui/react-popover, Command sayfası npm install cmdk komutunu verir; ardından bileşen kaynaklarını components/ui/popover.tsx ve components/ui/command.tsx dosyalarına kopyalayıp import yollarını projenize göre düzeltirsiniz. Command dosyası Dialog bileşenini de import ettiği için Dialog da projenizde bulunmalıdır. Örnekte ayrıca Button ve cn() yardımcı fonksiyonu kullanılır; ikisi de standart kurulumla gelir (cn() yazısı).
Temel örnek: adım adım combobox
Aşağıdaki kod resmî combobox-demo örneğinin yapısını birebir korur; yalnızca liste Türkçe şehir adlarıyla değiştirildi. Dosyayı örneğin components/sehir-combobox.tsx olarak kaydedebilirsiniz.
"use client"
import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
const sehirler = [
{ value: "istanbul", label: "İstanbul" },
{ value: "ankara", label: "Ankara" },
{ value: "izmir", label: "İzmir" },
{ value: "bursa", label: "Bursa" },
{ value: "antalya", label: "Antalya" },
]
export function SehirCombobox() {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState("")
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
>
{value
? sehirler.find((sehir) => sehir.value === value)?.label
: "Şehir seçin..."}
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Şehir arayın..." />
<CommandList>
<CommandEmpty>Şehir bulunamadı.</CommandEmpty>
<CommandGroup>
{sehirler.map((sehir) => (
<CommandItem
key={sehir.value}
value={sehir.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
setOpen(false)
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === sehir.value ? "opacity-100" : "opacity-0"
)}
/>
{sehir.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}Kodu yukarıdan aşağı okuyalım. Dosyanın başındaki "use client" satırı, bileşen useState kullandığı için gereklidir; Next.js App Router kullanıyorsanız bu satır olmadan kanca çalışmaz (bkz. Next.js kurulum yazısı). İki ayrı durum vardır: open panelin açık olup olmadığını, value ise seçili öğenin değerini tutar. Popover kontrollü kullanılır: open ve onOpenChange birlikte verildiği için paneli kodla kapatabilirsiniz.
<PopoverTrigger asChild> ifadesi, tetikleyicinin kendi düğmesini üretmek yerine içindeki Button bileşenini tetikleyici olarak kullanmasını sağlar. asChild mantığı bileşen özelleştirme yazısında ayrıntılı anlatılıyor. Button üzerindeki role="combobox" ve aria-expanded={open} öznitelikleri, ekran okuyuculara bu düğmenin bir seçim kutusu gibi davrandığını ve panelin açık olup olmadığını bildirir. Rol ve özniteliklerin genel anlamı için ARIA yazısına bakabilirsiniz.
Düğmenin etiketi, seçili değere karşılık gelen öğenin label alanından bulunur; değer boşsa yer tutucu metin görünür. Bu ayrım önemlidir: listede iki alan vardır, value makine için, label insan için. Resmî örnekte değerler küçük harfli, boşluksuz ya da sade kimliklerdir ve görünen metin her zaman find() ile ayrıca bulunur.
PopoverContent üzerindeki p-0 sınıfı tesadüf değildir. Depodaki popover.tsx kaynağında içeriğin varsayılan sınıfları arasında w-72 ve p-4 bulunur. Command kendi iç boşluklarını getirdiği için panel dolgusu sıfırlanır ve genişlik w-[200px] ile düğmeyle eşitlenir. cn() içindeki tailwind-merge sayesinde sizin verdiğiniz p-0, varsayılan p-4 değerini ezer.
Panelin içinde Command parçaları sırayla yer alır. CommandInput arama kutusudur; kaynakta solunda bir arama ikonu bulunan bir sarmalayıcı ile gelir. CommandList kaydırılabilir listedir; kaynakta max-h-[300px] ve dikey kaydırma sınıfları vardır, yani uzun listeler panel boyunu aşmaz. CommandEmpty arama sonucunda hiç öğe kalmadığında görünen metindir. CommandGroup öğeleri gruplar; Command sayfasındaki kullanım örneğinde olduğu gibi heading vererek gruba başlık da koyabilirsiniz.
Her CommandItem bir value ve bir onSelect alır. onSelect geri çağrısı seçilen öğenin değerini parametre olarak verir. Örnekteki currentValue === value ? "" : currentValue ifadesi küçük ama kullanışlı bir davranış ekler: zaten seçili olan öğeye tekrar tıklarsanız seçim kaldırılır. Hemen ardından setOpen(false) paneli kapatır. Son olarak CheckIcon her öğede vardır ama yalnızca seçili öğede opacity-100 olur; diğerlerinde görünmezdir. Bu sayede metinler hizalı kalır.
Parçaların görevleri

| Parça | Görevi | Örnekteki ayar |
|---|---|---|
| Popover | Açılır yüzeyin kökü, açık/kapalı durumunu yönetir | open ve onOpenChange ile kontrollü |
| PopoverTrigger | Paneli açan öğe | asChild ile Button kullanılır |
| PopoverContent | Portal içinde açılan panel | p-0 ve düğmeyle aynı genişlik |
| Command | cmdk tabanlı aranabilir liste kökü | Panelin tek çocuğu |
| CommandInput | Arama kutusu | placeholder metni |
| CommandList | Kaydırılabilir sonuç listesi | Empty ve Group bunun içinde |
| CommandEmpty | Sonuç yoksa gösterilen metin | Bulunamadı mesajı |
| CommandItem | Seçilebilir satır | value ve onSelect |
Popover örneği: nesne tutan durum
Belgedeki ikinci örnek (combobox-popover) aynı fikri farklı bir durum biçimiyle gösterir. Burada seçili değer bir metin değil, listedeki nesnenin kendisidir: React.useState<Status | null>(null). onSelect içinde gelen değerle listedeki nesne find() ile bulunur, bulunamazsa null atanır. Düğme etiketi de doğrudan selectedStatus.label olur. Bu biçim, seçimle birlikte başka alanlara (renk, ikon, kimlik) da ihtiyaç duyduğunuzda işinizi kolaylaştırır.
<PopoverContent className="p-0" side="right" align="start">
<Command>
<CommandInput placeholder="Durum değiştirin..." />
<CommandList>
<CommandEmpty>Sonuç bulunamadı.</CommandEmpty>
<CommandGroup>
{statuses.map((status) => (
<CommandItem
key={status.value}
value={status.value}
onSelect={(value) => {
setSelectedStatus(
statuses.find((priority) => priority.value === value) ||
null
)
setOpen(false)
}}
>
{status.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>Bu örnekte PopoverContent için side="right" ve align="start" verilmiştir; panel düğmenin altında değil sağında, üst kenara hizalı açılır. Kaynakta align için varsayılan değer center, sideOffset için 4 olarak tanımlıdır. Bu özelliklerin tümü Radix Popover içeriğine aktarılır; Popover ile Tooltip ve Hover Card farkları için popover yazısına bakın.
Responsive combobox: masaüstünde Popover, mobilde Drawer
Belge, masaüstünde <Popover />, mobilde <Drawer /> kullanarak responsive bir combobox kurulabileceğini söyler. Resmî combobox-responsive örneğinde liste ayrı bir StatusList bileşenine çıkarılır; böylece aynı liste iki farklı kabın içinde tekrar yazılmadan kullanılır. Kap seçimi bir medya sorgusuyla yapılır.
const [open, setOpen] = React.useState(false)
const isDesktop = useMediaQuery("(min-width: 768px)")
if (isDesktop) {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" className="w-[150px] justify-start">
{selectedStatus ? <>{selectedStatus.label}</> : <>+ Durum seç</>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<StatusList setOpen={setOpen} setSelectedStatus={setSelectedStatus} />
</PopoverContent>
</Popover>
)
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button variant="outline" className="w-[150px] justify-start">
{selectedStatus ? <>{selectedStatus.label}</> : <>+ Durum seç</>}
</Button>
</DrawerTrigger>
<DrawerContent>
<div className="mt-4 border-t">
<StatusList setOpen={setOpen} setSelectedStatus={setSelectedStatus} />
</div>
</DrawerContent>
</Drawer>
)Bu örnekteki useMediaQuery kancası bir shadcn/ui bileşeni değildir: örnek onu @/hooks/use-media-query yolundan import eder ve dosya depoda belge sitesinin kendi hooks klasöründe durur. Yani npx shadcn@latest add drawer komutu bu kancayı getirmez; kendiniz oluşturmanız gerekir. Depodaki sürüm kısadır (MIT lisanslı kod, shadcn-ui/ui GitHub deposu):
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 başlangıç değeri olarak false döndürür ve gerçek değeri ancak efekt çalıştıktan sonra öğrenir. Bu yüzden ilk çizimde bileşen mobil dalını (Drawer) seçer, ardından geniş ekranda Popover dalına geçer. Bu davranışı bilmek, sayfa ilk açıldığında kısa bir değişim gördüğünüzde nedenini anlamanızı sağlar. Depodaki bileşen kayıt klasöründe ayrıca hooks/use-mobile.ts içinde useIsMobile adlı benzer bir kanca bulunur; 768 piksel eşiğini kullanır ve sidebar bileşeni tarafından kullanılır. Drawer bileşeni ve onun Vaul tabanı Sheet ve Drawer yazısında anlatılıyor.
Combobox değerini bir forma bağlamak
Combobox kendi başına bir form alanı üretmez; değer bir React durumunda yaşar. Formda kullanırken yapacağınız iş, useState ile tutulan değeri form kütüphanesinin alan değeriyle değiştirmektir. React Hook Form kullanıyorsanız belgedeki Controller kalıbında field.value okunur ve seçimde field.onChange çağrılır; Select ve RadioGroup örneklerinde de tam olarak bu ikili kullanılır. Hata durumunu göstermek için tetikleyici düğmeye aria-invalid ve çevreleyen Field bileşenine data-invalid verilmesi, belgenin diğer kontroller için önerdiği yoldur. Ayrıntılar React Hook Form ve Zod yazısında, alan düzeni ise Field bileşeni yazısında.
TanStack Form tarafında aynı iş field.state.value ve field.handleChange ile yapılır (TanStack Form yazısı). Sunucu eylemleriyle çalışan ve FormData okuyan bir formda ise seçili değeri forma ulaştırmak için gizli bir input kullanmak gibi bir yol gerekebilir; bu durum resmî combobox belgesinde ele alınmaz, bu yüzden kendi testlerinizle doğrulayın.
Select mi, combobox mu?
Belge Combobox için "öneri listeli otomatik tamamlama girdisi ve komut paleti" tanımını kullanır. Select ise sabit bir seçenek listesinden seçim yaptıran klasik açılır kutudur ve arama kutusu içermez. Pratikte ayrım şudur: seçenekler azsa ve kullanıcı aramaya ihtiyaç duymuyorsa Select ya da Native Select yeterlidir; liste uzunsa ya da kullanıcının yazarak daraltması gerekiyorsa combobox mantıklıdır. Select ve Native Select farkları Select yazısında karşılaştırılıyor.
Sık yapılan hatalar

npx shadcn@latest add comboboxbeklemek: belge combobox için ayrı bir kurulum komutu vermez; Popover ve Command eklenir.PopoverContentüzerindep-0unutmak: varsayılanp-4dolgusu Command çevresinde fazladan boşluk bırakır.- Görünen metni
valueolarak kullanmak: değer ile etiketi ayrı tutun, etiketifind()ile bulun. Örnekler bu ayrımı korur. - Seçimden sonra paneli kapatmamak:
onSelectiçindesetOpen(false)yoksa panel açık kalır. CommandEmptyveCommandGroupöğeleriniCommandListdışına koymak: resmî örneklerin hepsinde ikisi de listenin içindedir.- Responsive örneği kopyalayıp
useMediaQuerydosyasını oluşturmamak: import çözülmez, çünkü kanca CLI ile gelmez. - İstemci bileşeni işaretini unutmak: kanca kullanan dosyada
"use client"yoksa App Router bunu sunucu bileşeni sayar.
Sık Sorulan Sorular
Birden fazla öğe seçilebilen bir combobox var mı?
Bu deponun Combobox belgesindeki dört örneğin hepsi tek seçimlidir. Çoklu seçim için Command öğelerini kendiniz bir dizi durumuna bağlayabilirsiniz, ancak bunun hazır bir resmî örneği bu sayfada yoktur.
Arama nasıl çalışıyor, kendim mi filtrelemeliyim?
Resmî örneklerde liste hiçbir yerde elle filtrelenmez; yazılan metne göre öğeleri daraltma işini cmdk tabanlı Command bileşeni yapar ve eşleşme kalmadığında CommandEmpty görünür. Filtreleme ayrıntıları için cmdk belgesine bakmanız gerekir.
Combobox Dialog içinde açılabilir mi?
Command dosyası CommandDialog adında, Command bileşenini bir Dialog içine yerleştiren hazır bir parça da sunar. Bu kullanım combobox değil komut paleti senaryosudur ve Command palette yazısında Ctrl/Cmd+K kısayoluyla birlikte anlatılıyor.
Base UI ile kurulan projelerde de aynı kod mu geçerli?
Bu yazı varsayılan new-york-v4 stilindeki Radix tabanlı kaynakları temel alır. Aralık 2025 değişiklik notunda duyurulan npx shadcn create akışı Base UI seçeneği de sunar; o tarafın ayrıntıları yenilikler yazısında.
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 · Command palette · Select ve Native Select · Popover, Tooltip, Hover Card · Sheet ve Drawer · React Hook Form ve Zod