Radix UI, React, Tailwind CSS ile Özelleştirilmiş Select Component Geliştirme Rehberi 🚀

Mehmet K.

--

Selamlar. Klasik HTML Select etiketinden sıkıldınız mı? Baştan bir component geliştirmek sizce de gereksiz efor değil mi? O zaman gelin Radix UI kullanarak bize sunduğu Primitive componentler ile istediğimizi, istediğimiz tasarım ile geliştirelim! Bugün Select componenti geliştiriyoruz 🥳

Ayrıca React, Next.js, Tailwind CSS, Typescript gibi teknolojileri kullanıyor olacağım.

Radix UI?

Radix UI, modern web uygulamalarında erişilebilir ve özelleştirilebilir kullanıcı arayüzleri oluşturmayı kolaylaştıran bir araçtır. Temelde düşük seviyeli bileşenleri ve araçları sağlar, böylece geliştiriciler karmaşık ve özgün kullanıcı deneyimleri oluşturabilirler.

Primitive Component Nedir?

ChatGPT açıklıyor: Kullanıcı arayüzünü oluşturmak için temel yapı taşlarıdır. Bu bileşenler, daha yüksek seviyeli bileşenlerin temelini oluşturur ve özelleştirilebilir, yeniden kullanılabilir UI elemanlarıdır.

Bilgilendirmelerimizi yaptığımıza göre hemen Select componentimizi geliştirmeye geçelim! Ben de adım adım bu componenti geliştireceğim, bu yüzden neyi nasıl nerede yapacağımızı daha iyi anlatabileceğim, let’s go!

Ortaya Ne Çıkacak?

Adım 1: Kullanılacak Paketler

yarn add clsx tailwind-merge @radix-ui/react-select
  • clsx, tailwind-merge: Bu paketler, birazdan oluşturacağımız cn() fonksiyonu için gerekli.
  • @radix-ui/react-select: Şşt, her şeyin özü burada 🤫

Adım 2: cn Fonksiyonu Oluşturmak

Öncelikle bu fonksiyonu açıklayayım. Bu fonksiyon sayesinde className kontrolünü çok kolay bir şekilde sağlayabiliyoruz. Yani oluşturduğumuz Select componentini daha sonra spesifik olarak düzenlemek istediğimizde (örneğin bg-red-500 olsun) bunu tailwind-merge ve clsx kullanarak oluşturabiliyoruz.

//app/lib/utils.ts
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

Adım 3: Select Componenti Oluşturmak

  • Next.js’in app-router (güncel) versiyonunda useState, useEffect gibi fonksiyonları ancak “use client” ibaresini belirttiğimizde kullanabiliyoruz. Yani bu componentimiz client taraflı render edilmeli.
  • * as RSelect şeklinde Select paketimizi import ediyoruz. Bu şekilde import ettiğimiz için RSelect.Root, RSelect.Trigger tarzında kullanacağız.
  • Öncelikle basit bir template oluşturalım ve önizleyelim.
  • “asChild”: Bu Radix UI’dan gelen bir özellik. Bunu yazdığımız zaman o kapsayıcı yok oluyor ve bütün özellik içine yazdığımız componente aktarılıyor. Bu şekilde kullanmak bana daha cazip geliyor açıkçası.
//app/Components/Select.tsx
"use client";

import * as RSelect from "@radix-ui/react-select";

export default function Select({}) {
return (
<RSelect.Root
onValueChange={(value) => {
console.log("Seçilen: ", value);
}}
>
<RSelect.Trigger asChild>
<button>
<RSelect.Value placeholder="Seçiniz" />
<RSelect.Icon />
</button>
</RSelect.Trigger>
<RSelect.Content>
<RSelect.Viewport>
{Array(10)
.fill(null)
.map((v, i) => (
<RSelect.Item value={`item-${i + 1}`}>
<RSelect.ItemText>Item {i + 1}</RSelect.ItemText>
</RSelect.Item>
))}
</RSelect.Viewport>
</RSelect.Content>
</RSelect.Root>
);
}

Herhangi bir className kullanmadığımız zaman görüntümüz bu şekilde oluyor, işte Radix UI bize bunu sağlıyor! Biz ise className’ler ekleyerek bu görüntüyü güzel bir şeye dönüştüreceğiz, yani umarım 😁

Adım 4: Güzelleştirmek

Şimdi gelin biraz düzenleyelim ve ortaya neler çıkacak bakalım:

//app/Components/Select.tsx
"use client";

import * as RSelect from "@radix-ui/react-select";

export default function Select({}) {
return (
<RSelect.Root
onValueChange={(value) => {
console.log("Seçilen: ", value);
}}
>
<RSelect.Trigger asChild>
<button className="outline-none flex items-center justify-center gap-2 bg-gray-50 hover:bg-gray-100 transition border rounded-md p-2 font-medium">
<RSelect.Value placeholder="Seçiniz" />
<RSelect.Icon asChild>
<svg
className="shrink-0 w-5 h-5"
aria-hidden="true"
viewBox="0 0 1024 1024"
style={{
display: "inline-block",
stroke: "currentcolor",
fill: "currentcolor",
}}
>
<path
d="M256 384l256 256 256-256"
fill="none"
stroke-linejoin="round"
stroke-linecap="round"
stroke-miterlimit="4"
stroke-width="128"
></path>
</svg>
</RSelect.Icon>
</button>
</RSelect.Trigger>
<RSelect.Content className="border rounded-md bg-gray-50">
<RSelect.Viewport>
{Array(10)
.fill(null)
.map((v, i) => (
<RSelect.Item
value={`item-${i + 1}`}
className="outline-none cursor-pointer p-2 font-medium bg-transparent hover:bg-gray-100 transition"
>
<RSelect.ItemText>Item {i + 1}</RSelect.ItemText>
</RSelect.Item>
))}
</RSelect.Viewport>
</RSelect.Content>
</RSelect.Root>
);
}

Bence bir öncekine göre daha cool görünüyor 😅

Adım 5: Dinamikleştirmek

E bu Array(10).fill(null)… bla bla bundan mı ibaret olacak sandınız? Pehh… 😂 Şimdi gelin biraz dinamikleştirelim.

Placeholder dinamik şekilde alalım, girilmediyse “Seçiniz” görünsün.

“list” diye bir parametre ile seçenekleri ekleyeceğiz. Bu Select componentinde tek statik olacak parametre budur. Tabii bu da dinamik olacak ama type’ı statik olacak. Şu type’ı kullanacağız (Benim ihtiyacımı bu görüyor):

{ value: string; label: string }

Dinamikleştirdikten sonra geldiği hâl:

//app/Components/Select.tsx
"use client";

import * as RSelect from "@radix-ui/react-select";

interface Props {
placeholder?: string;
list: { value: string; label: string }[];
onValueChange: (value: string) => void;
}

export default function Select({ placeholder, list, onValueChange }: Props) {
return (
<RSelect.Root onValueChange={onValueChange}>
<RSelect.Trigger asChild>
<button className="outline-none flex items-center justify-between gap-2 bg-gray-50 hover:bg-gray-100 transition border rounded-md p-2 font-medium">
<RSelect.Value placeholder={placeholder ?? "Seçiniz"} />
<RSelect.Icon asChild>
<svg
className="shrink-0 w-5 h-5"
aria-hidden="true"
viewBox="0 0 1024 1024"
style={{
display: "inline-block",
stroke: "currentcolor",
fill: "currentcolor",
}}
>
<path
d="M256 384l256 256 256-256"
fill="none"
strokeLinejoin="round"
strokeLinecap="round"
strokeMiterlimit="4"
strokeWidth="128"
></path>
</svg>
</RSelect.Icon>
</button>
</RSelect.Trigger>
<RSelect.Content className="border rounded-md bg-gray-50">
<RSelect.Viewport>
{list.map((item, i) => (
<RSelect.Item
key={i}
value={item.value}
className="outline-none cursor-pointer p-2 font-medium bg-transparent hover:bg-gray-100 transition"
>
<RSelect.ItemText>{item.label}</RSelect.ItemText>
</RSelect.Item>
))}
</RSelect.Viewport>
</RSelect.Content>
</RSelect.Root>
);
}

Kullanım:

<Select
placeholder="Bi' şeyler seç"
list={Array(10)
.fill(null)
.map((v, i) => ({
value: `item-${i + 1}`,
label: `Item ${i + 1}`,
}))}
onValueChange={(value) => {
console.log("Seçilen: ", value);
}}
/>

Adım 6: Spesifik Güzelleştirmek

Burada ise spesifik şekilde her bir Select componentine özel className’ler yazmak istiyoruz ama aynı altyapıyı kullanmak istiyoruz. O zaman gelin şöyle bir yöntem kullanalım.

classNames diye bir parametre (obje) oluşturalım. Ardından hangi componenti özelleştirmek istiyorsak ona göre bu objeye key ekleyelim. İlk olarak ben button, content ve item componentlerini güncellemek istiyorum, o zaman şöyle kullanabilirim:

classNames?: {
button?: string;
content?: string;
item?: string;
};

Bu şekilde yönetmek bana daha kolay geliyor, bu yüzden bu yolu kullanıyorum.

Eklediklerimizi cn() fonksiyonu kullanarak componentlerimize entegre edelim:

//app/Components/Select.tsx
"use client";

import { cn } from "@/app/lib/utils";
import * as RSelect from "@radix-ui/react-select";

interface Props {
placeholder?: string;
list: { value: string; label: string }[];
onValueChange: (value: string) => void;
classNames?: {
button?: string;
content?: string;
item?: string;
};
}

export default function Select({
classNames,
placeholder,
list,
onValueChange,
}: Props) {
return (
<RSelect.Root onValueChange={onValueChange}>
<RSelect.Trigger asChild>
<button
className={cn(
"outline-none flex items-center justify-between gap-2 bg-gray-50 hover:bg-gray-100 transition border rounded-md p-2 font-medium",
classNames?.button
)}
>
<RSelect.Value placeholder={placeholder ?? "Seçiniz"} />
<RSelect.Icon asChild>
<svg
className="shrink-0 w-5 h-5"
aria-hidden="true"
viewBox="0 0 1024 1024"
style={{
display: "inline-block",
stroke: "currentcolor",
fill: "currentcolor",
}}
>
<path
d="M256 384l256 256 256-256"
fill="none"
strokeLinejoin="round"
strokeLinecap="round"
strokeMiterlimit="4"
strokeWidth="128"
></path>
</svg>
</RSelect.Icon>
</button>
</RSelect.Trigger>
<RSelect.Content
className={cn("border rounded-md bg-gray-50", classNames?.content)}
>
<RSelect.Viewport>
{list.map((item, i) => (
<RSelect.Item
key={i}
value={item.value}
className={cn(
"outline-none cursor-pointer p-2 font-medium bg-transparent hover:bg-gray-100 transition",
classNames?.item
)}
>
<RSelect.ItemText>{item.label}</RSelect.ItemText>
</RSelect.Item>
))}
</RSelect.Viewport>
</RSelect.Content>
</RSelect.Root>
);
}

Hadi şimdi başka bir tane Select componenti oluşturalım ve özelleştirelimm:

<Select
placeholder="Bi' şeyler seç"
list={Array(10)
.fill(null)
.map((v, i) => ({
value: `item-${i + 1}`,
label: `Item ${i + 1}`,
}))}
onValueChange={(value) => {
console.log("Seçilen: ", value);
}}
/>
<Select
placeholder="Renkli menkli"
list={Array(10)
.fill(null)
.map((v, i) => ({
value: `item-${i + 1}`,
label: `Item ${i + 1}`,
}))}
onValueChange={(value) => {
console.log("Seçilen: ", value);
}}
classNames={{
button:
"bg-red-100 border-red-300 text-red-500 hover:bg-red-200 px-3 w-[200px]",
content: "bg-red-100 border-red-300 text-red-500",
item: "hover:bg-red-200 px-3",
}}
/>

Eveeet! Yapacaklarımız işte bu kadardı 😂 Şimdi CodeSandbox üzerinden önizlemek isteyenler için link bırakıyorum:

Bu şekilde vakit buldukça Radix UI’da bulunan diğer Primitive Component’leri anlatmayı düşünüyorum. Umarım faydalı oluyordur, münkün olduğunca daha az kodla daha çeşitli işler yapmayı hedeflediğim için bu tarz componentler ortaya çıkıyor, ki böylesi daha hoş bence.

Son olarak ufak bi’ reklamm.

Kişisel Sitem: https://konukcu.dev/
GitHub Profilim: https://github.com/mehmetext
Bikodist Instagram Sayfam: https://www.instagram.com/bikodist
LinkedIn Profilim: https://www.linkedin.com/in/mehmetkonukcu

--

--

Mehmet K.
Mehmet K.

No responses yet