Skip to content
CodeSook
CodeSook

Tailwind Variants

สวัสดีครับเพื่อนๆ วันนี้ผมจะพาเพื่อนๆทุกคนมาทำความรู้จักกับ Tailwind Variants กันครับ

What is Tailwind Variants

Tailwind Variants คือ first-class variant API library สำหรับ Tailwind CSS ซึ่งก็คือเครื่องมือที่ช่วยเราจัดการคลาสของ Tailwind CSS ให้เป็นระบบและระเบียบมากขึ้นครับ โดยเฉพาะเวลาที่ components มีหลายรูปแบบ (variants) เช่น หลายขนาด หลายสี และที่ถูกเรียกใช้ซ้ำหลายๆครั้ง


The Problem Without Tailwind Variants

ปัญหาที่เรามักพบเจอหากไม่ได้ใช้ Tailwind Variants และถึงเวลาที่ Project ของเราเริ่มมึขนาดใหญ่ขึ้น และมี components เพิ่มมากขึ้นได้แก่

  • className เริ่มมีความยาวขึ้นมาก ทำให้โค๊ดอ่านยาก
  • style ของแต่ละ component มีความซ้ำซ้อน โดยมีค่าแค่ที่ต่างกันเพียงไม่กี่อย่าง เช่น สี หรือ ขนาด แต่ยังคงต้องเขียน className อันใหม่ ยาว และซ้ำกันที่ไปมาทุกครั้งที่ต้องการใช้ components ที่คล้ายกับอันเดิมที่เคยเขียนไว้แล้ว
  • หากเราต้องการแก้ไข style ของ components ประเภทเดียวกันทั้งหมด ยกตัวอย่างเช่น Button, Card หรือ Aside ที่กระจายอยู่หลายที่ใน project เรา ทำให้เราต้องตามหาและตามไปแก้ในทุกจุด ทีละอันจนหมด ซึ่งทำให้เสียเวลาและมีโอกาสที่เราอาจจะลืมแก้บาง components ไปได้

How Tailwind Variants Helps

Tailwind Variants จะช่วยจัดการปัญหาเหล่านี้ด้วยการทำให้การจัดการสไตล์เป็นแบบ declarative และรวมศูนย์ไว้ในที่เดียว

ข้อดีของ Tailwind Variants ก็คือ

  • แยก Logic ออกจาก Style ได้อย่างชัดเจน ทำให้โค๊ดอ่านง่ายขึ่น
  • มี type safety สามารถใช้งานร่วมกับ TypeScript ได้ดี
  • แทนที่เราจะสร้าง component หลายอัน (เช่น <ButtonPrimary />, <ButtonSecondary />, …) เราสามารถสร้างแค่ component เดียวโดยใช้ variants (e.g., <Button variant="primary" />) และ reuse ได้ในทุกๆที่

Basic Examples

ตอนนี้ทุกคนได้รู้ข้อดีของ Tailwind Variants ไปแล้ว ผมจะมายกตัวอย่างวิธีใข้งานและเปรียบเทียบให้ดูระหว่างสถานการณ์ที่เราใช้และไม่ได้ใช้ Tailwind Variants ให้ดูกัน

หาก project ของเพื่อนๆยังไม่เคยติดตั้ง Tailwind CSS ต้องติดตั้งก่อนด้วยนะครับ ไม่งั้น Tailwind Variants จะไม่ทำงาน เราจะมาเริ่มจากการจะติดตั้ง tailwind-variants และ tailwind-merge ลงใน project ของเรากัน

Terminal window
bun add tailwind-variants && bun add tailwind-merge

Example With Tailwind Variants

ตัวอย่างที่ใช้ Tailwind Variants

ButtonVariant.tsx
import type { FC } from 'react';
import { tv, type VariantProps } from 'tailwind-variants';
const button = tv({
    base: 'font-semibold text-white text-sm py-1 px-4 rounded-full active:opacity-80 hover:cursor-pointer',
    variants: {
        color: {
            primary: 'bg-blue-500 hover:bg-blue-700',
            secondary: 'bg-purple-500 hover:bg-purple-700',
            success: 'bg-green-500 hover:bg-green-700'
        }
    }
});
type Props = {
    text: string
} & VariantProps<typeof button>
const ButtonVariant: FC<Props> = ({ text, color }) => {
    return <button className={button({ color })}>{text}</button>;
};
export default ButtonVariant;

base คือการประกาศค่า default ของ style นั้นๆ ซึ่งเวลาเรานำ component นี้ไปเรียกใช้ ไม่ว่าเราจะเลือก color เป็น “primary”, “secondary” หรือ “success” style ของ base ก็จะติดมาด้วยเสมอ

จะเห็นได้ว่าในโค๊ดนี้นั้น

  • แยก style ออกมาอีกส่วน ทำให้โค๊ดของเราอ่านง่ายขึ้น
  • Type-safe และ มี auto complete เมื่อต้องการเลือก color ตอนนำไปเรียกใช้
App.tsx

หลักจากนั้นเราก็ import เข้ามาใช้งานได้เลย

import ButtonVariant from "./components/ButtonVariant";
export function App() {
return (
<div className="flex gap-4">
<ButtonVariant text="Submit" color="success" />
<ButtonVariant text="Confirm" color="primary" />
<ButtonVariant text="Cancel" color="secondary" />
</div>
);
}
export default App;

หน้าตาของปุ่มเราก็จะเป็นประมาณนี้


Example Without Tailwind Variants

ตัวอย่างที่ไม่ได้ใช้ Tailwind Variants

Button.tsx
import type { FC } from 'react';
type Props = {
  text: string;
  color?: 'primary' | 'secondary' | 'success';
};
const Button: FC<Props> = ({ text, color = 'primary' }) => {
  return (
    <button
      className={`font-semibold text-white text-sm py-1 px-4 rounded-full active:opacity-80 hover:cursor-pointer
        ${color === 'primary' && 'bg-blue-500 hover:bg-blue-700'}
        ${color === 'secondary' && 'bg-purple-500 hover:bg-purple-700'}
        ${color === 'success' && 'bg-green-500 hover:bg-green-700'}
      `}
    >
      {text}
    </button>
  );
};
export default Button;

ในโค๊ดนี้เราไม่ได้ใช้ Tailwind Variants ทำให้

  • className ยาวและอ่านยาก
  • Logic ตรง color === ‘primary’ นั้นต้องมาอยู่ในส่วนของ tsx.
  • ถ้าเราต้องการเพิ่ม variant ใหม่ เท่ากับเราต้องเพิ่มเงื่อนไขใหม่อีกด้วย

Tailwind Variants Slots Example

รอบนี้เีราจะใช้ icon จาก lucide-react ด้วยครับ เพราะฉะนั้นเรามาติดตั้งกันก่อน

Terminal window
bun add lucide-react
Card.tsx
import { Layers } from "lucide-react"
import { tv, type VariantProps } from "tailwind-variants"
const card = tv({
    slots: {
        base: "grid grid-cols-12 w-md h-26 rounded-lg overflow-hidden mt-2 bg-white text-amber-950 border border-1 border-b-2 border-amber-950",
        rightSide: "flex justify-center items-center col-span-3",
        textContainer: "col-span-9 flex flex-col items-start p-4 justify-center",
        descriptionText: "font-bold text-2xl"
    },
    variants: {
        bg: {
            blue: {
                rightSide: "bg-blue-400"
            },
            green: {
                rightSide: "bg-green-200"
            },
            orange: {
                rightSide: "bg-orange-200"
            },
            pink: {
                rightSide: "bg-pink-200"
            }
        }
    }
})
type CardVariants = VariantProps<typeof card>
type Props = {
    title: string
    description: string
    icon?: React.ReactNode
} & CardVariants
export function Card({ bg, title = "title", description = "description", icon = <Layers /> }: Props) {
    const { base, rightSide, descriptionText, textContainer } = card({ bg })
    return (
        <div className={base()}>
            <div className={textContainer()}>
                <p>{title}</p>
                <p className={descriptionText()}>{description}</p>
            </div>
            <div className={rightSide({ bg })}>
                {icon}
            </div>
        </div>
    )
}
export default Card

ครั้งนี้เรามีการใช้ slots ซึ่งสามารถทำให้เราแยก component ออกมาเป็นหลายชื้นย่อยๆได้ เหมาะสำหรับ component ที่มีโครงสร้างซับซ้อน และมีหลายส่วนที่จำเป็นต้องควบคุม style แยกกัน

App.tsx

หลักจากนั้นเราก็ import เข้ามาใช้งานได้เลย

import { Book, GraduationCap, Inbox, } from "lucide-react"
import Card from "./components/Card";
export function App() {
return (
<div className="flex gap-4">
<div className="flex flex-col gap-4">
<Card bg="blue" title="Total" description="40" />
<Card bg="green" title="Mastered" description="11" icon={<GraduationCap />} />
<Card bg="orange" title="In progress" description="21" icon={<Book />} />
<Card bg="pink" title="Not started" description="8" icon={<Inbox />} />
</div>
</div>
);
}
export default App;

หน้าตาของการ์ดเราก็จะเป็นประมาณนี้

Conclusion

Tailwind Variant ช่วยลด duplication ของ className และ แยก logic ออกจาก style ทำให้โค๊ดเราสะอาด อ่านง่ายขึ้น และยังช่วยเราประหยัดเวลาจากการทำซ้ำไปอีก

ถ้า project ของเพื่อนๆมีขนาดที่เริ่มใหญ่ขึ้น หรือเริ่มมี component เดิมๆซ้ำๆ Tailwind Variants คือเครื่องมือที่ควรลองอย่างยื่งครับ แต่ถ้าหากเป็น component เล็กๆที่ใช้เพียงครั้งเดียว การเขียนแบบ className ตรงๆก็ยังเป็นทางเลือกที่โอเคครับ แต่ทั้งนี้ผมว่ายังไงเพื่อนๆก็ได้นำไปใช้แน่นอน

Thank you🙏🏻

สำหรับใครที่อ่านมาถึงตรงนี้ ผมขอขอบคุณมากๆครับ หวังว่า Blog นี้จะช่วยให้ทุกคนรู้จักและเข้าใจ Tailwind Variants มากขึ้นนะครับ จริงๆ Tailwind Variants สามารถทำได้มากกว่านี้อีกครับ ใน Blog นี้เป็นเพียงพื้นฐานเพื่อทำความรู้จักเท่านั้น แต่ก็เพียงพอที่จำนำไปใช้ให่เกิดประโยชน์ได้ หากสนใจเพิ่มเติมสามารถไปลองอ่านเพิ่มเติมได้ที่ Documentation ของ Tailwind Variants นะครับ ขอบคุณทุกคนอีกครั้งครับ.