Next.js (React) ve Tailwind CSS ile Varyantlara Sahip Componentler Geliştirmek

Mehmet K.
8 min readMay 7, 2023

--

Selam, yeniden! 😄 Bugün yine karşınıza Next.js ve Tailwind CSS ile çıkıyorumm. Varyantlara sahip componentler geliştirmek başlarda bana çok zorlu ve sıkıcı geliyordu fakat birazdan anlatacağım yol sayesinde işler çok kolaylaştı, benim işim kolaylaşmışken hemen size de anlatmak istedim.

Bu konuyu anlatmaya çalışırken Next.js, Tailwind CSS, Atomic Design, Typescript gibi teknolojilerden yardım alacağım. Kusurum olursa şimdiden affola 😊

Canlı örnek: https://component-variants-next.vercel.app/ (Repository linki aşağıdadır.)

Öncelikle yapmamız gereken componenti düşünelim… Evet düşündüm ve olmazsa olmaz olan “Button.tsx” yapalım dedim 😅 Bu butonu Figma’da çizdiğimizi hayal edelim.

Butonun varyantları şunlar olacak:

  • size = {sm, md, lg}
  • color = {blue, red, green}

Öncelikle md ve blue varyanlarına sahip basit bir buton geliştirelim hemen. Ayrıca button.tsx dosyasını “src/components/atoms/Button.tsx” yolunda oluşturuyorum, çünkü atomic design yapısına uygun ilerlemek istiyorum. Aslında bu basit şey için gerekli değil fakat karşılaştığım herkese “Atomic Design” öneriyorum, bu yüzden hadi buna uyalım 😆:

//React
import {ReactNode} from "react"

//Props
interface Props {
children?: ReactNode
}

export default function Button({children}: Props) {
return <button className="bg-blue-500 hover:bg-blue-600 px-4 py-2 rounded-md text-white ">{children}</button>
}

Şimdi buna varyanları nasıl ekleyeceğiz, akıllarda deli sorular 🤣 Öncelikle sakin olalım… Burada paketler kullanacağız. Hatta “src/utils/cn.ts” yoluna bir yardımcı fonksiyon da ekleyeceğiz. Bu fonksiyonu https://ui.shadcn.com/ sitesinden alıyorum:

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

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

Burada “clsx” ve “tailwind-merge” paketlerini yüklemeliyiz. Bu fonksiyon bize aynı işi gören classları sadeleştirebilmemiz konusunda yardımcı oluyor. Aynı classtan 2 tane varsa veya Tailwind’de “flex” ve “block” gibi aynı işi gören farklı classlar varsa daha önceki classı silip yeni ekleneni kullanıyor.

Ayrıca belirtmeliyim ki burada anlattığım her şeyi ben de shadcn adlı hocamızdan öğrendim, benim şu an yaptığım ise Türkçe bir kaynak oluşturmaktır.

cn fonksiyonunu ekledikten sonra şu hale geldi kodlarımız:

//React
import {ReactNode} from "react"

//Utils
import {cn} from "@/utils/cn"

//Props
interface Props {
children?: ReactNode
}

export default function Button({children}: Props) {
return <button className={cn("bg-blue-500 hover:bg-blue-600 px-4 py-2 rounded-md text-white")}>{children}</button>
}

Şimdi burada hala işimiz çok. Bizim düşünmemiz gereken şu:

  • color için: “bg-blue-500 hover:bg-blue-600”
  • size için: “px-4 py-2 rounded-md”
  • Geriye kalanlar: “text-white”

Bu şekilde kafamızda classları ayırmalıyız, birazdan işimize yarayacak çünkü.

“class-variance-authority” adlı bir paket daha yüklemeliyiz. Bizim asıl işimizi bu paket çözecek. Bu paket, yazdığımız varyantlara göre classlar üretiyor. Aynı classları üretirse diye de “cn” fonksiyonunu kullanıyoruz işte. Bu paketi de dahil ettikten sonra şu şekilde bir kod ürettim:

//React
import {ReactNode} from "react"

//Utils
import {cn} from "@/utils/cn"

//cva
import {cva} from "class-variance-authority"

//Props
interface Props {
children?: ReactNode
}

const buttonVariants = cva("SABIT_CLASSLAR", {
variants: {
color: {
blue: "BLUE_CLASSLAR"
},
size: {
md: "MD_CLASSLAR"
}
}
})

export default function Button({children}: Props) {
return <button className={cn("bg-blue-500 hover:bg-blue-600 px-4 py-2 rounded-md text-white")}>{children}</button>
}

Burada fark ettiyseniz “buttonVariants” adlı bir değişken oluşturarak cva fonksiyonunu kullandım. cva fonksiyonu bizden bir string ve bir obje bekliyor. Objenin içinde “variants” adlı bir obje var, bu obje bizim işimizi çözecek olan kahraman işte.

  • “SABIT_CLASSLAR”: Buraya adında da belli olduğu gibi sabit classlarımızı buraya yazarız. Örneğin “text-white”, “shadow” gibi.
  • “BLUE_CLASSLAR”: Buraya ise color: blue varyantına sahip olduğu zamanki classları yazmalıyız.
  • “MD_CLASSLAR” Buraya da size: md varyantına sahip olduğu zamanki classları yazmalıyız.

Güncelledikten sonra şu hali almalı:

const buttonVariants = cva("text-white", {
variants: {
color: {
blue: "bg-blue-500 hover:bg-blue-600"
},
size: {
md: "px-4 py-2 rounded-md"
}
}
})

Şimdi bunu butonumuzda kullanmalıyız:

//React
import {ReactNode} from "react"

//Utils
import {cn} from "@/utils/cn"

//cva
import {cva} from "class-variance-authority"

//Props
interface Props {
children?: ReactNode
}

const buttonVariants = cva("text-white", {
variants: {
color: {
blue: "bg-blue-500 hover:bg-blue-600"
},
size: {
md: "px-4 py-2 rounded-md"
}
}
})

export default function Button({children}: Props) {
return <button className={cn(buttonVariants({color: "blue", size: "md"}))}>{children}</button>
}

Şimdi biraz daha oturdu, değil mi? Buraya kadar anladıysanız bence her şey on numara, eğer anlatamadıysam ve iyi bir ingilizceye sahipseniz “https://ui.shadcn.com/” bu siteden okumanızı kesinlikle tavsiye ederim. Neyse hadi devam edelimm.

Burada hala elimizle parametre veriyoruz fakat bunun bir atom olduğunu biliyoruz, bu yüzden bütün ayarlarını bir üst componentten yönetebilmeliyiz. Typescript kullandığımız için “class-variance-authority” paketinden bir şey daha almamız gerek, o da bu: “VariantProps”

Şimdiii “Props” değişkenimizi güncelleyelim:

//Props
interface Props extends VariantProps<typeof buttonVariants> {
children?: ReactNode
}

Props da tamamsa kodumuzu tekrar güncelleyelim:

//React
import {ReactNode} from "react"

//Utils
import {cn} from "@/utils/cn"

//cva
import {cva} from "class-variance-authority"
import {VariantProps} from "class-variance-authority"

//Props
interface Props extends VariantProps<typeof buttonVariants> {
children?: ReactNode
}

const buttonVariants = cva("text-white", {
variants: {
color: {
blue: "bg-blue-500 hover:bg-blue-600"
},
size: {
md: "px-4 py-2 rounded-md"
}
}
})

export default function Button({children, color, size}: Props) {
return <button className={cn(buttonVariants({color: color, size: size}))}>{children}</button>
}

O da ne? Component kayboldu 😂 Sakinn, eğer gidip şöyle yaparsak sorun kalmayacak:

<Button size="md" color="blue">
md-blue button
</Button>

Biz bunu istemiyoruz ama, bizim istediğimiz <Button> koyduğumuzda default varyantlarımız olsun. O zaman hemen cva fonksiyonumuzu şu şekilde güncellemeliyiz:

const buttonVariants = cva("text-white", {
variants: {
color: {
blue: "bg-blue-500 hover:bg-blue-600"
},
size: {
md: "px-4 py-2 rounded-md"
}
},
defaultVariants: {
color: "blue",
size: "md"
}
})

Şimdi şu şekilde kullanırsak default olarak “md-blue” varyantına sahip olmuş oluruz:

<Button size="md" color="blue">
md-blue button
</Button>

Şimdi gelin de diğer size varyantlarını ekleyelim, ayrıca ben text classı da ekledim:

const buttonVariants = cva("text-white", {
variants: {
color: {
blue: "bg-blue-500 hover:bg-blue-600"
},
size: {
sm: "px-2 py-1 rounded text-sm",
md: "px-4 py-2 rounded-md text-base",
lg: "px-6 py-4 rounded-lg text-xl"
}
},
defaultVariants: {
color: "blue",
size: "md"
}
})
<Button size="sm">sm-blue button</Button>
<Button>md-blue button</Button>
<Button size="lg">lg-blue button</Button>

Görüntümüz ise artık şu oldu:

color varyantlarını da şöyle eklerseeek:

const buttonVariants = cva("text-white", {
variants: {
color: {
blue: "bg-blue-500 hover:bg-blue-600",
red: "bg-red-500 hover:bg-red-600",
green: "bg-green-500 hover:bg-green-600"
},
size: {
sm: "px-2 py-1 rounded text-sm",
md: "px-4 py-2 rounded-md text-base",
lg: "px-6 py-4 rounded-lg text-xl"
}
},
defaultVariants: {
color: "blue",
size: "md"
}
})
      <div className="flex flex-col gap-2 items-center">
<Button size="sm">sm-blue button</Button>
<Button>md-blue button</Button>
<Button size="lg">lg-blue button</Button>
</div>
<div className="flex flex-col gap-2 items-center">
<Button size="sm" color="red">
sm-red button
</Button>
<Button color="red">md-red button</Button>
<Button size="lg" color="red">
lg-red button
</Button>
</div>
<div className="flex flex-col gap-2 items-center">
<Button size="sm" color="green">
sm-green button
</Button>
<Button color="green">md-green button</Button>
<Button size="lg" color="green">
lg-green button
</Button>
</div>

Görüntümüz artık böyledir:

Biraz güncelleme yapalım ve soluna da ikon ekleyelim, dimi yani:

      <div className="flex items-center justify-center gap-2">
<Button color="green" size="sm">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" className="h-4 w-5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>{" "}
Onayla
</Button>
<Button color="green">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" className="h-4 w-5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>{" "}
Onayla
</Button>
<Button color="green" size="lg">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" className="h-4 w-5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>{" "}
Onayla
</Button>
</div>
//React
import {ReactNode} from "react"

//Utils
import {cn} from "@/utils/cn"

//cva
import {cva} from "class-variance-authority"
import {VariantProps} from "class-variance-authority"

//Props
interface Props extends VariantProps<typeof buttonVariants> {
children?: ReactNode
}

const buttonVariants = cva("text-white flex items-center justify-center", {
variants: {
color: {
blue: "bg-blue-500 hover:bg-blue-600",
red: "bg-red-500 hover:bg-red-600",
green: "bg-green-500 hover:bg-green-600"
},
size: {
sm: "px-2 py-1 rounded text-sm gap-1",
md: "px-4 py-2 rounded-md text-base gap-2",
lg: "px-6 py-4 rounded-lg text-xl gap-4"
}
},
defaultVariants: {
color: "blue",
size: "md"
}
})

export default function Button({children, color, size}: Props) {
return <button className={cn(buttonVariants({color: color, size: size}))}>{children}</button>
}

Bir şey daha ekleyebiliriz, mesela her buton için custom class yazmamız gerek, onu da şu şekilde ekleyebiliriz:

//React
import {ReactNode} from "react"

//Utils
import {cn} from "@/utils/cn"

//cva
import {cva} from "class-variance-authority"
import {VariantProps} from "class-variance-authority"

//Props
interface Props extends VariantProps<typeof buttonVariants> {
children?: ReactNode
className?: string
}

const buttonVariants = cva("text-white flex items-center justify-center", {
variants: {
color: {
blue: "bg-blue-500 hover:bg-blue-600",
red: "bg-red-500 hover:bg-red-600",
green: "bg-green-500 hover:bg-green-600"
},
size: {
sm: "px-2 py-1 rounded text-sm gap-1",
md: "px-4 py-2 rounded-md text-base gap-2",
lg: "px-6 py-4 rounded-lg text-xl gap-4"
}
},
defaultVariants: {
color: "blue",
size: "md"
}
})

export default function Button({children, className, color, size}: Props) {
return <button className={cn(buttonVariants({color: color, size: size}), className)}>{children}</button>
}
        <Button size="lg" className="rounded-full">
rounded-full button
</Button>

Her şey hazır, artık istediğimiz yerde <Button> diyerek çağırır ve kullanırız, kimse de karışamazz 🤓 Bundan sonraki yaratıcılık tamamen Figma’daki çizime veya size kalmış, hadi kolay gelsiin.

Repository: https://github.com/mehmetext/component-variants-next

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

--

--