twMerge + classNames

Lager du ny nettside med Tailwind som styling-rammeverk? Da bør du begynne å bruke følgende kodesnutt istedenfor classnames når du skal slå sammen klasser i Tailwind.‍



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

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

Sammenslåing av klassenavn på en god måte

Mange som lager ny nettside bruker Tailwind som rammeverk for styling. Én av utfordringene alle som har skrevet Tailwind har opplevd er å måtte slå sammen klasser dynamisk. Eksempelvis:



export function MyComponent{}{
   const [isOpen, setIsOpen] = useState(false)

	return (
		
{/* ... */}
) }

Utfordringen med dette kommer når man ikke bare har én variabel å forholde seg til, men mange. Eller når klassene man skal bruke betinget er mer komplekse:


export function MyComponent(){
   const [isOpen, setIsOpen] = useState(false)
   const [highlightText, setHighlightText] = useState(false)

	return (
		
{/* ... */}
) }

Og kanskje man vil hente inn klassenavn fra en ovenliggende komponent?



export function MyComponent(classNames){
   const [isOpen, setIsOpen] = useState(false)
   const [highlightText, setHighlightText] = useState(false)

	return (
		
{/* ... */}
) }

Som du kanskje ser er det lagt til tomme mellomrom etter “p-4”, “block” og “hidden”. Dette er for å passe på at den resulterende klassen er noe som: bg-white p-4 block text-yellow-500 leading-5 og ikke bg-white p-4blocktext-yellow-500.

Etterhvert som bruken blir mer kompleks kan dette være vanskelig å holde styr på. Spesielt om du har veldig lange klassenavn, som er vanlig med Tailwind.

En løsning på dette er classnames biblioteket. Eller dens lettvektige lillebror clsx. La oss ta utgangspunkt i clsx for resten av artikkelen.

Vi kan skrive om koden over på to forskjellige måter med vårt nye verktøy:



import clsx from 'clsx'

export function MyComponent(classNames){
   const [isOpen, setIsOpen] = useState(false)
   const [highlightText, setHighlightText] = useState(false)

	return (
		
	)
}



import clsx from 'clsx'

export function MyComponent(classNames){
   const [isOpen, setIsOpen] = useState(false)
   const [highlightText, setHighlightText] = useState(false)

	return (
		
	)
}

clsx (og classnames) fikser automatisk mellomrom for oss, og er meget fleksibel på inputten vi sender inn. Det er f.eks. ikke noe problem om «classNames» er undefined.

Så da er alt bra, eller?

Problemer med sammenslåing konflikterende klassenavn

En utfordring oppstår når vi prøver å slå sammen klasser som gir lignende instruksjoner:



import clsx from 'clsx'

export function MyComponent(classNames){
   const [isOpen, setIsOpen] = useState(false)
   const [highlightText, setHighlightText] = useState(false)

	return (
		
	)
}

export function ParentComponent(){
  return (
    
  )
}

Hva forventer du at paddingen på MyComponent sin div er? Om du er som meg tenker du at, vel, det resulterende klassenavnet i MyComponent blir bg-white p-4 hidden text-black p-6, og da blir paddingen i p-6 (padding: 1.5rem;) anvendt, ja?

Nei. Slik som HTML og CSS fungerer vil p-4 være den dominerende paddingen, og overskrivingen vår av defaultverdien vil ikke fungere.

Åpenbare løsninger på dette problemet kommer fort til kort når man må håndtere mer kompliserte tilfeller som p-4 px-2 pb-3 p-5 py-3 .

Heldigvis finnes det et bibliotek for dette: tailwind-merge. Tailwind-merge ligner litt på classNames og clsx i måten det tar input på:



twMerge('px-2 py-1 bg-red hover:bg-dark-red', 'p-3 bg-[#B91C1C]')
// → 'hover:bg-dark-red p-3 bg-[#B91C1C]'

Utfordringen med tailwind-merge er derimot at det ikke er like fleksibelt på hva det tar imot av input, og kan f.eks. ikke håndterer javascript objekter som vi brukte over:



twMerge("bg-white p-4 hidden text-black", 
	{
	  "block": isOpen,
    "text-yellow-500 leading-5": highlightText
  },
	classNames
)
// → 💥

Løsningen

Så hva er løsningen på begge våre problemer? Jo, å slå sammen twMerge og clsx:



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

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

Endrer vi nå komponenten vår til:



import cn from '~/utils/cn'

export function MyComponent(classNames){
   const [isOpen, setIsOpen] = useState(false)
   const [highlightText, setHighlightText] = useState(false)

	return (
		
	)
}

export function ParentComponent(){
  return (
    
  )
}

Er vi i mål. Komponenten vises med p-6 some førende padding, og alle klassenavn blir slått sammen på riktig måte.

Inspirasjon hentet her og her.

Skrevet av
Tormod Haugland

Andre artikler