Tailwind CSS Academy · บทเรียน

หลีกเลี่ยงคลาสขัดแย้งด้วย tailwind-merge

ทำความเข้าใจการทำงานของความจำเพาะของคลาส Tailwind และใช้ tailwind-merge เพื่อให้รูปแบบที่ใช้ทีหลังสุดมีผล โดยไม่เกิดข้อผิดพลาดด้านความจำเพาะ

บทเรียน 4 จาก 413 ขั้นตอน

หลีกเลี่ยงคลาสขัดแย้งด้วย tailwind-merge เป็นบทเรียน Tailwind CSS Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Tailwind CSS Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Tailwind CSS Academy มีบทเรียนทั้งหมด 4 บทเรียน

บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ

How Tailwind Class Conflicts Arise

Tailwind utility classes set individual CSS properties. When two classes target the same property — like p-4 and p-8, or text-blue-500 and text-red-500 — both end up in the element's class list. The browser resolves the conflict using CSS cascade order: whichever utility was generated later in Tailwind's stylesheet wins, regardless of the order in your HTML. This makes overriding parent component styles unpredictable without tailwind-merge.

<!-- Both p-4 and p-8 appear in the DOM -->
<div class="p-4 p-8">...</div>
<!-- Which padding is applied? Depends on Tailwind's
     stylesheet order, not the class string order. -->

<!-- Same problem with text colors -->
<div class="text-gray-900 text-blue-500">...</div>
<!-- Will the text be gray or blue? You cannot be sure. -->

What tailwind-merge Does

tailwind-merge is a runtime utility that analyzes a class string and removes conflicting Tailwind classes, keeping only the last class from each conflicting group. It has an internal map of which Tailwind utilities conflict with each other — understanding that p-4 and p-8 both set padding, or that font-bold and font-medium both set font-weight. The last class in the input string always wins.

import { twMerge } from 'tailwind-merge';

// Conflict resolution: last value wins
twMerge('p-4 p-8')
// Output: 'p-8'

twMerge('text-gray-900 text-blue-500')
// Output: 'text-blue-500'

twMerge('font-bold font-medium text-sm text-lg')
// Output: 'font-medium text-lg'

// Non-conflicting classes are kept
twMerge('flex items-center gap-4 p-4')
// Output: 'flex items-center gap-4 p-4'

Installing and Basic Usage

Install tailwind-merge as a production dependency (not dev-only, since it runs at runtime). Import twMerge and wrap any class string that might have conflicts. The function accepts multiple arguments and merges them all, similar to how clsx accepts multiple arguments — making it easy to drop into existing code.

npm install tailwind-merge

import { twMerge } from 'tailwind-merge';

// Single string
const cls = twMerge('bg-blue-500 bg-red-500');
// → 'bg-red-500'

// Multiple arguments (like clsx)
const cls2 = twMerge(
  'px-4 py-2 rounded',   // base
  'px-8',                // override padding-x
  'text-white'
);
// → 'py-2 rounded px-8 text-white'

twMerge Understands Utility Groups

tailwind-merge understands Tailwind's full utility taxonomy. It knows that px-4 sets horizontal padding while py-2 sets vertical padding, so they do not conflict. It knows that shadow-md and shadow-lg both set the box-shadow property and will conflict. It also handles variants like hover:bg-blue-500 and hover:bg-red-500 as a separate conflict group from their non-variant counterparts.

import { twMerge } from 'tailwind-merge';

// px and py don't conflict with each other
twMerge('px-4 py-2 px-8')
// → 'py-2 px-8'  (px-4 removed, px-8 wins)

// Hover variants are separate groups
twMerge('hover:bg-blue-500 hover:bg-red-500 bg-white')
// → 'bg-white hover:bg-red-500'

// Shadow variants
twMerge('shadow-sm shadow-lg shadow-md')
// → 'shadow-md'

// Responsive prefixes are separate groups
twMerge('md:text-xl md:text-2xl text-sm')
// → 'text-sm md:text-2xl'

The cn() Helper Pattern

The standard pattern in React/Next.js projects is to combine clsx and twMerge into a single cn() helper function. clsx handles conditional class logic and filters falsy values; twMerge then resolves any conflicts in the resulting string. Define this once in your utils file and use it everywhere — this is the approach used by shadcn/ui and most modern Tailwind component libraries.

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

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

// Usage: conditional classes with conflict resolution
function Badge({ active, className }) {
  return (
    <span
      className={cn(
        'px-2 py-1 rounded-full text-sm font-medium',
        active ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-600',
        className  // caller override — twMerge resolves conflicts
      )}
    >
      Label
    </span>
  );
}

className Prop Override Pattern

The primary use case for tailwind-merge in React components is enabling safe className prop overrides. When a component has default styles and a caller provides additional or replacement classes via the className prop, twMerge ensures the caller's intent is honored. This makes components genuinely customizable without requiring consumers to fight CSS specificity or use !important.

// Without twMerge: caller override might not work
function Card({ className, children }) {
  return (
    <div className={'bg-white rounded-xl p-6 shadow ' + className}>
      {children}
    </div>
  );
}
<Card className='bg-gray-50' /> // bg-white and bg-gray-50 both present!

// With twMerge: caller override always wins
function Card({ className, children }) {
  return (
    <div className={cn('bg-white rounded-xl p-6 shadow', className)}>
      {children}
    </div>
  );
}
<Card className='bg-gray-50' /> // only bg-gray-50 (bg-white removed)

Arbitrary Values and twMerge

tailwind-merge also handles arbitrary value classes using Tailwind's bracket notation. It correctly identifies that p-[10px] conflicts with p-4 and that w-[300px] conflicts with w-full. This is important because arbitrary values are common when designing components that need precise measurements not available in the default scale.

import { twMerge } from 'tailwind-merge';

// Arbitrary values conflict with standard utilities
twMerge('p-4 p-[10px]')
// → 'p-[10px]'  (last wins)

twMerge('w-full w-[300px]')
// → 'w-[300px]'

twMerge('text-sm text-[15px]')
// → 'text-[15px]'

// Mix of standard and arbitrary
twMerge('bg-blue-500 bg-[#2563eb]')
// → 'bg-[#2563eb]'

Configuring twMerge for Custom Classes

Out of the box, tailwind-merge only knows about Tailwind's default utilities. If you have added custom utilities via plugins or the config, twMerge will not know they conflict with related defaults. Use extendTailwindMerge() to teach twMerge about your custom classes, ensuring conflict resolution works correctly for your extended utility set.

import { extendTailwindMerge } from 'tailwind-merge';

// Tell twMerge about custom text-shadow utilities
const customTwMerge = extendTailwindMerge({
  extend: {
    classGroups: {
      'text-shadow': ['text-shadow-sm', 'text-shadow-md', 'text-shadow-lg', 'text-shadow-none']
    }
  }
});

// Now conflicts are resolved correctly
customTwMerge('text-shadow-sm text-shadow-lg')
// → 'text-shadow-lg'

// Update cn() to use the custom instance
export const cn = (...inputs) => customTwMerge(clsx(inputs));

Performance Considerations

tailwind-merge parses and resolves class strings at runtime in the browser. For most applications, this is imperceptibly fast. However, components that render thousands of instances (like virtual lists or table cells) may benefit from caching the resolved class string. tailwind-merge provides a createTailwindMerge() factory that enables a custom cache adapter. For most apps, the default cache (which uses an LRU internally) is sufficient.

import { createTailwindMerge, getDefaultConfig } from 'tailwind-merge';

// Create a version with a larger cache for high-volume usage
const twMerge = createTailwindMerge(getDefaultConfig);

// For critical performance: memoize with React.useMemo
function VirtualListItem({ selected, disabled, className }) {
  const itemClass = useMemo(
    () => cn(
      'flex items-center px-3 py-2',
      selected && 'bg-blue-50',
      disabled && 'opacity-50',
      className
    ),
    [selected, disabled, className]
  );
  return <div className={itemClass}>{/* ... */}</div>;
}

Common Pitfalls With tailwind-merge

tailwind-merge has a few edge cases to be aware of. It does not handle CSS Modules classes — only Tailwind utility names. If a class string contains non-Tailwind classes (like custom BEM classes), twMerge passes them through unchanged. Also, some plugin-generated utilities may not be recognized without extending the merge config. Lastly, twMerge operates on the class string, not on the rendered CSS, so it cannot resolve conflicts caused by CSS cascade beyond Tailwind utilities.

import { twMerge } from 'tailwind-merge';

// Non-Tailwind classes pass through untouched
twMerge('card__header bg-white bg-gray-50')
// → 'card__header bg-gray-50'  (custom class kept, Tailwind conflict resolved)

// CSS Modules classes (hash-based) also pass through
twMerge('styles__button_abc123 bg-blue-500 bg-red-500')
// → 'styles__button_abc123 bg-red-500'

// Unknown plugin classes (without extendTailwindMerge) pass through
twMerge('text-shadow-lg text-shadow-sm')  // both kept if not configured
// → 'text-shadow-lg text-shadow-sm' (no conflict detected)

Testing With tailwind-merge

When writing unit tests for components that use cn(), test the actual class string output to verify that conflicts are resolved correctly. This is especially valuable for component libraries where callers must be able to override defaults reliably. Snapshot tests for className outputs ensure that future refactors do not accidentally break the override behavior.

// card.test.ts
import { cn } from '@/lib/utils';
import { cardVariants } from './Card';

test('caller className overrides default bg', () => {
  const result = cn(cardVariants({ variant: 'white' }), 'bg-gray-50');
  // bg-white from variant should be removed, bg-gray-50 should win
  expect(result).not.toContain('bg-white');
  expect(result).toContain('bg-gray-50');
});

test('non-conflicting classes are additive', () => {
  const result = cn('p-6 rounded-xl', 'mt-4');
  expect(result).toContain('p-6');
  expect(result).toContain('rounded-xl');
  expect(result).toContain('mt-4');
});

Quick Check

Test your understanding of Tailwind CSS Mastery concepts from this lesson.

Lesson Recap

In this lesson you learned: tailwind-merge resolves conflicting Tailwind utilities by keeping only the last class from each conflict group, the cn() helper combines clsx and twMerge for conditional and conflict-free class handling, and extendTailwindMerge() teaches the library about custom plugin classes. Next up we explore Headless UI and how it integrates with Tailwind for accessible components.

เริ่มต้นได้ฟรี

เรียนรู้ HTML ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
30
บทเรียน
120

คำถามที่พบบ่อย

บทเรียน “หลีกเลี่ยงคลาสขัดแย้งด้วย tailwind-merge” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “หลีกเลี่ยงคลาสขัดแย้งด้วย tailwind-merge” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Tailwind CSS Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Tailwind CSS Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “หลีกเลี่ยงคลาสขัดแย้งด้วย tailwind-merge”

ทำความเข้าใจการทำงานของความจำเพาะของคลาส Tailwind และใช้ tailwind-merge เพื่อให้รูปแบบที่ใช้ทีหลังสุดมีผล โดยไม่เกิดข้อผิดพลาดด้านความจำเพาะ คุณปฏิบัติ Tailwind CSS Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Tailwind CSS Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Tailwind CSS Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “หลีกเลี่ยงคลาสขัดแย้งด้วย tailwind-merge” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Tailwind CSS Academy นี้ได้ไหม

ได้ บทเรียน Tailwind CSS Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การตั้งค่า Tailwind ใน Next.js
  2. คลาสแบบมีเงื่อนไขใน React
  3. รูปแบบคอมโพเนนต์ด้วย CVA
  4. หลีกเลี่ยงคลาสขัดแย้งด้วย tailwind-merge
← กลับไปที่ Tailwind CSS Academy