Skip to main content

Tailwind CSS เทคนิคและแนวปฏิบัติแบบ Best Practices

Kongvut Sangkla

Intro

สวัสดีครับ บทความนี้จะพูดถึง Tailwind CSS สำหรับคนที่เคยใช้งานมาแล้ว เนื่องจากเทคโนโลยีที่ได้ใช้งานบ่อยใช้เยอะช่วงนี้ พอใช้เรียนรู้และใช้งานแบบ Basic มาได้สักพัก ก็ไปค้นหาข้อมูลที่คนอื่น ๆ เขาเขียนและนำนำไว้เพื่อดูเทคนิค (Tips) และแนวปฏิบัติที่ดีที่สุด (Best Practices) ทำให้เจอข้อมูลที่น่าสนใจหลายอย่าง

บทความนี้จึงเป็นการรวบรวมและประเด็นที่น่าสนใจที่ค้นพบ และโน้ต ๆ ไว้อ่าน 🚀

Tailwind CSS

Tailwind CSS คือ Utility-First CSS Framework เป็น CSS Selector แบบสำเร็จที่เรามานำ Class เล็ก ๆ มาประกอบกันขึ้นอยู่กับวัตถุประสงค์การใช้งานสามารถนำไป Implement ได้หลายแบบ เช่น

  • ใช้แค่ Tailwind เพียว ๆ อย่าง Tailwind UI
  • Tailwind CSS + Ant-Design
  • Tailwind CSS + Material UI
  • Tailwind CSS + UI อื่น ๆ

Best Practices

1. ใช้ States แทนการใช้ Booleans

ระวังการใช้ Booleans

<Button primary disabled secondary active />

ถ้าสังเกตปัญหาของวิธีข้างบน คือการจัดเรียงที่เป็นแบบ invalid มากเกินไปทำให้อ่านยาก

<Button state={Button.state.ACTIVE} variant={Button.variant.PRIMARY} />

ทำให้ชัดไปเลยไม่มีทางที่จะไม่เป็น primary และไม่มีทางที่จะไม่เป็น active

2. ClassName เป็น prop ที่อาจจะเป็น code smell

<Button variant={Button.variant.PRIMARY} className="p-4" />

เมื่อสร้าง Button แบบฉบับ Version ของตัวเอง แต่มันยากที่จะรู้ว่าอะไรอยู่ข้างใน State บ้าง (อาจจะมี className อีกตัวในนั้น) ที่จะทำให้ ClassName ที่เป็น prop อาจจะเป็น code smell

3. ใช้ local state แทน global state

เพื่อให้ชัดว่า State มาจากไหน

4. ใช้ props แทน state

มันจะดีกว่าเรื่องประสิทธิภาพถ้าใช้ props แทนการ sync ค่าจาก state

5. เป็นไปได้ไม่ต้องใช้ props เลยจะดีมาก

ใช้ Props เท่าที่จำเป็น หรือไม่ต้องใช้เลย

6. ใช้ components แทน props

<Button big fullWidth/> 
// ควรเป็น
<BigButton/>
// หรือ
<FullWidthButton/>

7. ใช้ children แทน props

<Button icon={''}>Apple</Button>
// ควรเป็น
<Button>Apple</Button>
import React, { ReactNode } from 'react';
import { classNames } from '../util/classNames';
enum Variant {
RED,
YELLOW,
GREEN,
BLUE,
}
enum Size {
LARGE,
SMALL,
}
type Props = {
variant: Variant;
children?: ReactNode;
size: Size;
};
const SIZE_MAPS: Record<Size, string> = {
[Size.SMALL]: 'px-2.5 text-xs',
[Size.LARGE]: 'px-3 text-sm',
};
const VARIANT_MAPS: Record<Variant, string> = {
[Variant.RED]: 'bg-red-100 text-red-800',
[Variant.YELLOW]: 'bg-yellow-100 text-yellow-800',
[Variant.GREEN]: 'bg-green-100 text-green-800',
[Variant.BLUE]: 'bg-blue-100 text-blue-800',
};
export function Badge(props: Props) {
const { children, variant, size } = props;
return (
<span
className={classNames(
'inline-flex items-center py-0.5 rounded-full font-medium leading-4 whitespace-no-wrap',
VARIANT_MAPS[variant],
SIZE_MAPS[size],
)}
>
{children}
</span>
);
}
Badge.defaultProps = {
variant: Variant.GRAY,
size: Size.SMALL,
};
Badge.variant = Variant;
Badge.size = Size;
<Badge variant={Badge.variant.RED} size={Badge.size.BIG}>Text</Badge>

เอาจริง ๆ ประเด็นนี้ทำวิธีแบบนี้ก็ดี ✅ และถ้าจะไม่ใช้ก็ไม่ได้ซีเรียส 😌 เพราะว่าถ้าเขียนเป็น Typescript ใช้กับ IDE ที่ฉลาด ๆ หน่อยเราจะรู้ได้หมดกว่า Component นั้น ๆ กำหนด props ไว้แบบไหนบ้าง

Types of Tailwind components

Non-components (snippets)

บางครั้งสิ่งที่คุณพยายามทำเป็นรูปแบบซ้ำ ๆ (Components) แต่แทนที่จะเป็น Component หากรายละเอียดจะไม่ซ้ำกันสำหรับ Component ส่วนใหญ่ และไม่จำเป็นต้องเชื่อมโยงกับ Component อื่นๆ มันก็คือ Non-components จะเรียกว่า Template หรือ Snippet

ตัวอย่างที่ดีของเรื่องนี้คืออะไร ? อย่าง Tailwind ก็มีแบบจ่ายเงินและแบบฟรีสำหรับใช้งานคือ Tailwind UI พวกนี้ก็เป็น Snippets ที่สามารถคัดแล้ววางใส่ใน App เพื่อสร้าง Prototyping แบบเร็ว ๆ พวกมันอาจกลายเป็นส่วนประกอบที่ใช้ซ้ำได้ในโค้ดของคุณในบางจุด

Small components

ตัวอย่างปุ่มใช้ 17 Utility classes และบางทีคุณอาจจะมี 12 ปุ่มที่มีการแชร์ 15 utility classes จาก 17 classes

<button type="button" class="inline-flex items-center px-2.5 py-1.5 border 
border-transparent text-xs font-medium rounded shadow-sm text-white
bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2
focus:ring-offset-2 focus:ring-indigo-500">
Button text
</button>

ใน tailwind เคสแบบนี้คุณอาจจะใช้ @apply directive เพื่อรวม common classes เพื่อสร้างเป็น class ปุ่มแบบใหม่ แต่เอาจริง ๆ วิธีนี้ส่วนตัวกลับไม่ค่อยชอบ เพราะว่าวิธีชอบวิธีการสร้าง components ประกอบกันและใช้วิธี class compose เวลาดูแลรักษามันอ่านง่ายกว่า

Small component (code smell)

ในบางเคสคุณอาจจะต้องการสร้าง Component สำหรับห่อหุ้มบางสิ่งจาก Styles อื่น เช่น การคลิก Event เพื่อ Handler นี่คือข้อยกเว้นสำเรื่อง classNames แบบ props ที่่จะเป็น code smell และ เป็นการดีที่จะใช้ classNames เป็น prop และใส่ Utility classes บน component ที่ต้องการ

Large component (complex)

ใน components ขนาดใหญ่อาจจะประกอบไปด้วย Elements หลายอัน เพื่อกำหนดส่วนประกอบที่เป็นได้มากที่สุด และการใส่ค่าต่าง ๆ ลงใน props แต่มันก็ยากมากในการอ่านโค้ดเหล่านั้นและเข้าใจ State ของแต่ละ Version

จากการพยายามแทนที่ด้วย components over props (6) และ children over components (7) แต่เมื่อ App เติบโตขึ้นและเปลี่ยนแปลง แต่ละกรณีมีแนวโน้มที่จะแตกต่างกัน ทำให้คุณต้องเพิ่ม prop อื่นเพื่อรองรับและทำงานได้กับ Features ใหม่ ๆ

Large component (complex) เมื่อพวกมันมักจะเชื่อมโยงกัน จนกว่าคุณจะพบว่าคุณมี Component ปีศาจ ☠️ เมื่อถึงจุด ๆ นึงจะเข้าใจว่า นี่คือบทเรียนสำคัญ: อย่ากลัวที่จะทำลายส่วนประกอบขนาดใหญ่เหล่านั้นเมื่อมันเทอะทะ ❌

Utility classes

  1. ใช้ Utility classes น้อย ๆ เท่าที่เป็นไปได้ ตัวอย่าง mx-2 แทน ml-2 mr-2 และอย่ากังวลที่จะใช้ p-4 lg:pt-8 แทน pt-4 lg:pt-8 pr-4 pb-4 pl-4 ที่ยาวและซับซ้อน
  2. พิจารณาการใช้ prefix ตัวอย่าง block lg:flex lg:flex-col lg:justify-center แทน block lg:flex flex-col justify-center เพราะจะเคลียร์กว่า โดย flexbox จะใช้กับ lg: ที่เป็น breakpoint และ ขนาดใหญ่ larger
  3. สิ่งนึงที่ควรจำไว้คือ โดยปกติ responsive utilities ทั้งหมดจะกำหนด min-width นั่นคือ sm: แต่กลายเป็นว่ามันไม่ได้ apply แค่กับ small viewports เท่านั้นเพราะจะ apply กับ small breakpoints และ large ซึ่งสำหรับตัวอย่าง lock sm:block md:flex lg:flex xl:flex คุณสามารถใช้แค่ md:flex ซึ่งมันก็ให้ผลลัพธ์เหมือนกัน
  4. อีกสิ่งนึงที่ควรรู้คือ sm: breakpoint โดยปกติจะไม่ได้เริ่มจาก min-width: 0 แบบที่คุณคิด แต่มันเริ่มที่ min-width: 640px ซึ่งถ้าคุณต้องการปรับให้เล็กที่สุด (เล็กกว่า 640px) คุณควรตั้งค่าให้ apply ทุก breakpoints และจากนั้น override สำหรับทุก larger breakpoints ตัวอย่าง block sm:inline ถ้าคุณต้องการ block สำหรับ smallest
  5. เวลาจะทำอะไรเกี่ยวกับ margin ใช้ mt-* และ ml-* ควบคู่กันสำหรับ position ของ current element หรือใช้ mb-* และ mr-* สำหรับ position elements ที่ใกล้ชิดกัน
  6. สามารถใช้ <div> เป็น wrapper เพื่อทำ padding แทนการทำ margins ที่อาจจะช่วยหลีกเลี่ยงปัญหาที่อาจจะเกิดขึ้นกับ margin collapsing เพราะไม่จำเป็นต้องพยายามทำ classname เดียวสำหรับแก้ปัญหานี้

Component classes

  1. อย่าพยายามใช้ @apply หรือสร้าง class ใหม่ใด ๆ ถ้าไม่จำเป็นนอกเสียจากว่า utility classes ภายใน component มีการใช้ซ้ำร่วมกับ components อื่น ๆ จริงซึ่งในเคสแบบนี้การสร้าง component class ใหม่ด้วย @apply อาจจะโอเค
  2. อย่า @apply กับ component classes ใน components อื่น ตัวอย่างเช่นแยก .btn และ .btn-blue classes ออกจากันแทนการใช้แบบ .btn-blue { @apply btn; }

Dynamic classes

  1. เมื่อใช้ CSS classes หรือเป็นแบบ Generated dynamically อย่าใช้ string ที่เป็นแบบต่อกัน (concatenation) เพื่อสร้าง Full class แต่ให้ใช้วิธีการสลับระหว่าง Strings class ตัวอย่างใช้ { maxPerRow == 2 ? 'sm:w-1/2' : 'sm:w-1/3' } แทน { 'sm:w-1/' + maxPerRow } เพราะจะเป็นปัญหาสร้าง class ใหม่แทนการใช้ class ที่มีอยู่และส่งผลกับขนาดไฟล์ css เมื่อ build ด้วย PurgeCSS

Bonus

Function สำหรับใช้ combine เพื่อสร้าง Full string จาก utility classes

export function classNames(...classes: (false | null | undefined | string)[]) {
return classes.filter(Boolean).join(" ");
}

// ตัวอย่างการใช้งาน
<button className={classNames('this is always applied',
isTruthy && 'this only when the isTruthy is truthy',
active ? 'active classes' : 'inactive classes')}>Text</button>

References

Loading...