0. tailwind css 설치
1. tailwind-merge, clsx 라이브러리 설치
npm i --save tailwind-merge
tailwind-merge 라이브러리에서 twMerge와 twJoin이 있는데 twMerge를 사용할 것입니다. twMerge 함수를 사용하게 되면 예를 들어 다음과 같은 코드에서 힘을 발휘합니다.
import React from 'react'
type Ttest = React.HTMLAttributes<HTMLElement>
const Test = (props:Ttest) => {
return (
<div className={`px-2 ${props.className}`} {...props}>Test</div>
)
}
export default Test
Test 컴포넌트의 prop으로 className="p-10" 이 들어왔을 때 어떻게 될까요? px-2가 이미 있기 때문에 py-10만 적용되고 px-2로 바뀌지 않게 됩니다. 이를 해결해 줄 수 있는 것이 twMerge입니다.
이 현상에 대해서 자세하게 다룬 좋은 🔗참고 글이 있습니다.
clsx는 기본(베이스) 유틸리티 클래스(아래 코드에서는 'button')에 추가적으로 들어올 수 있는 클래스명('active', 'disabled' 등이 되겠죠?)을 조건부(isActive, isDisabled 등)에 따라서 쉽게 부여할 수 있도록 도와주는 라이브러리입니다. 아래 코드를 보시면 한 번에 이해되실 겁니다.
import clsx from 'clsx';
const isActive = true;
const isDisabled = false;
const buttonClasses = clsx('button', {
'active': isActive,
'disabled': isDisabled,
});
clsx 공식 문서에서 tailwind와 사용할 때는 다음과 같이 settings.json을 셋하라고 되어 있네요.
{
"tailwindCSS.experimental.classRegex": [
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}
보통 이 둘을 합쳐서 유틸함수로 만들어서 사용합니다.
import { ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
/**
*
* @param inputs 이전 클래스를 override 하지 않기 위해서 spread operator를 쓰는 것이다.
* @returns
*/
const cn = (...inputs:ClassValue[]) => {
return twMerge(clsx(inputs))
}
export default cn
1-1. tailwind intellisense extension자동완성을 위한 정규표현식 설정.
tailwind를 사용 중이시라면 tailwind intellisense extenstion을 사용하고 계실 겁니다. 아니라면 무조건 확장 설치해주세요.
이 인텔리센스가 다른 곳에서도 작동하도록 하려면 2가지 방법(정규표현식 추가, tailwind 클래스에 추가)을 같이 쓰면 좋습니다. 첫 번째는 공식문서를 보고 그대로 복사해서 settings.json(사용자 설정)에 그대로 붙여 넣어줍니다. 각 tawilwind 프로퍼티는 처음 설정하는 거라면 아예 없는 것이 정상이므로 써 넣을 곳 찾지 마시고 새로 추가하시면 됩니다.
"tailwindCSS.experimental.classRegex": [
["(?:twMerge|twJoin)\\(([^\\);]*)[\\);]", "[`'\"`]([^'\"`,;]*)[`'\"`]"]
],

https://github.com/paolotiu/tailwind-intellisense-regex-list#tailwind-merge
GitHub - paolotiu/tailwind-intellisense-regex-list
Contribute to paolotiu/tailwind-intellisense-regex-list development by creating an account on GitHub.
github.com
두 번째로 tailwindCSS의 class로 인식하도록 속성을 추가해줍니다.
tailwind 유틸리티 클래스를 다른 변수에 담아 쓰고 싶을 때 자동완성이 안 되는 불편함을 해소해줍니다.
".*Styles.*" 는 Styles 글자가 포함된 식별자일 경우 tailwind의 클래스로 인식하여 intellisense가 작동하도록 해줍니다.
타입 지정이 있을 경우에도 대응할 수 있도록 정규표현식을 짰습니 tailwind의 클래스로 인식하여 intellisense가 작동하도록 해줍니다.
"tailwindCSS.classAttributes": [
"class",
"className",
"ngClass",
".*Styles.*"
]


https://github.com/tailwindlabs/tailwindcss/discussions/7554
[IntelliSense] Custom class name completion contexts · tailwindlabs/tailwindcss · Discussion #7554
There has been quite a few requests for the extension to support class name completions in contexts other than a standard class(Name) attribute. Some examples: tailwind.macro and twin.macro (#46, t...
github.com
2. CVA 설치
npm i class-variance-authority
2-1. CVA tailwind intellisense 정규표현식 추가
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] 는 cva 정규표현식입니다. cva 함수 내부에서 동작할 수 있게 해줍니다.
":\\s*?[\"'`]([^\"'`]*).*?," 는 자바스크립트 객체 내(프로퍼티 포함)에서 동작하도록 하기위한 코드입니다.
일반 ".*Styles.*"는 맨 처음 객체 내에서만 동작합니다. 이를 통해 해결할 수 있습니다.
twMerge, twJoin, clsx, cva 세팅까지 끝나면 다음과 같은 모습이 됩니다.
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["(?:twMerge|twJoin)\\(([^\\);]*)[\\);]", "[`'\"`]([^'\"`,;]*)[`'\"`]"],
":\\s*?[\"'`]([^\"'`]*).*?,"
],
"tailwindCSS.classAttributes": [
"class",
"className",
"ngClass",
".*Styles.*",
]
cva를 활용하여 좀 더 atomic 단위로 컴포넌트 관리가 가능해집니다.
활용 예시(스토리북에 적용 하였음)
/* Button.tsx */
import cn from '@/utils/cn';
import { cva, VariantProps } from 'class-variance-authority';
import React from 'react';
import '../tailwind.css';
export const ButtonVariants = cva(
/**
* @description 컴포넌트 @apply 도 잘 적용된다. cva 첫 번째 인자는 base: ClassValue 기본 깔고 가는 스타일이다. 없으면 그냥 "" 써주면 된다.
*/
`f-ic-jc active:scale-95 rounded-xl text-sm font-bold text-slate-100 transition-all shadow-md hover:scale-105 duration-200
`,
{
variants: {
/**
* 버튼 색깔
*/
intendedColor: {
/**
* @description 그냥 "shadow-sm bg-red-500" 이런 식으로 안에 적어도 되지만 좀 더 구분이 쉽게 하려면 [..., ...] 이런 식으로 적어도 된다.
*/
primary: ['shadow-lg', 'bg-red-700'],
secondary: ['shadow-none', 'bg-blue-700', 'ring ring-yellow-400'],
},
/**
* 버튼 크기
*/
size: {
default: '',
// md: 'w-[6.875rem] h-[2.375rem] text-[1rem] rounded-md',
md: 'px-14 h-[2.375rem] text-[1rem] rounded-md',
// lg: [' w-[21.875rem] h-[7.5rem] text-[3rem] rounded-2xl'],
lg: ' px-36 h-[7.5rem] text-[3rem] rounded-2xl',
wlg: 'w-[24rem] h-[5.25rem] text-[2rem]',
},
/**
* 대소문자
*/
textTransform: {
default: '',
capitalize: 'capitalize',
lowercase: 'lowercase',
uppercase: 'uppercase',
},
/**
* 에러용 버튼인지
*/
isError: {
true: true,
false: false,
},
/**
* @see https://manuarora.in/boxshadows
* box-shadow
*/
boxShadow: {
default: 'shadow-md',
Aesthetic: 'shadow-[0_3px_10px_rgb(0,0,0,0.2)]',
Euphonious: 'shadow-[0px_10px_1px_rgba(221,_221,_221,_1),_0_10px_20px_rgba(204,_204,_204,_1)]',
Jubilation: 'shadow-[rgba(0,_0,_0,_0.24)_0px_3px_8px]',
Mondegreen:
'shadow-[5px_5px_rgba(0,_98,_90,_0.4),_10px_10px_rgba(0,_98,_90,_0.3),_15px_15px_rgba(0,_98,_90,_0.2),_20px_20px_rgba(0,_98,_90,_0.1),_25px_25px_rgba(0,_98,_90,_0.05)]',
Nimble: 'shadow-[4.0px_8.0px_8.0px_rgba(0,0,0,0.38)]',
Ragnarok:
'shadow-[0_2.8px_2.2px_rgba(0,_0,_0,_0.034),_0_6.7px_5.3px_rgba(0,_0,_0,_0.048),_0_12.5px_10px_rgba(0,_0,_0,_0.06),_0_22.3px_17.9px_rgba(0,_0,_0,_0.072),_0_41.8px_33.4px_rgba(0,_0,_0,_0.086),_0_100px_80px_rgba(0,_0,_0,_0.12)]',
Stiglitz: 'shadow-[rgba(50,50,93,0.25)_0px_6px_12px_-2px,_rgba(0,0,0,0.3)_0px_3px_7px_-3px]',
},
},
/**
* @description 조건 충족하면 className 으로 가져온 추가 속성이 추가 적용됨.
*/
compoundVariants: [
{
size: 'md',
className: 'max-md:bg-primary',
},
{
size: 'lg',
className: 'max-lg:bg-secondary max-lg:px-10',
},
{
size: 'wlg',
className: 'uppercase ring ring-yellow-400',
},
{
isError: true,
className: 'text-red-500',
},
],
defaultVariants: {
intendedColor: 'primary',
size: 'default',
textTransform: 'default',
isError: false,
boxShadow: 'default',
},
},
);
// export type ButtonVariantProps = {
// label?: string | number;
// } & React.HTMLAttributes<HTMLButtonElement> &
// VariantProps<typeof ButtonVariants>;
export type ButtonVariantProps = {
/**
* 버튼 이름
*/
label?: string | number;
/**
* 버튼 이름 또는 svg 등
*/
children?: React.ReactNode;
/**
* 추가할 커스텀 스타일
*/
className?: string;
/**
* 에러가 발생했는지 여부
*/
isError?: boolean;
/**
* 마우스 클릭 시 콜백함수
*/
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/**
* 마우스 호버 시 콜백함수
*/
onMouseOver?: () => void;
/**
* 버튼 키보드로 눌릴 시 콜백함수
*/
onKeyDown?: () => void;
} & VariantProps<typeof ButtonVariants>;
// & ComponentProps<'button'>;
// React.HTMLAttributes<HTMLButtonElement>;
/**
* @description 필수 속성 추가한 타입이다. intendedColor는 무조건 입력할 수 있게 했다.
*/
export interface IbuttonProps
extends Omit<ButtonVariantProps, 'intendedColor'>,
Required<Pick<ButtonVariantProps, 'intendedColor'>> {}
const Button = ({
children,
className,
intendedColor,
size,
label,
textTransform,
isError,
boxShadow,
onClick,
onMouseOver,
onKeyDown,
}: IbuttonProps) => {
/**
* @description twMerge가 있어야 cva 첫 번째 인자 base classValue를 덮을 수 있음.
*/
// return <button className={ButtonVariants({intendedColor, size, className})} {...rest}>{label? label : children}</button>
return (
<button
className={cn(
ButtonVariants({
intendedColor,
size,
className,
textTransform,
isError,
boxShadow,
}),
)}
onClick={onClick}
onMouseOver={onMouseOver}
onKeyDown={onKeyDown}
>
{label ? label : children}
</button>
);
};
export default Button;
https://ubah484.hashnode.dev/building-reusable-components-in-react-using-tailwind-and-cva
Build Reusable UI Components in React and Tailwind CSS
Learn how to build reusable components in React & Tailwind and CVA
ubah484.hashnode.dev
https://www.youtube.com/watch?v=B6FrDu2Qbt0
https://cva.style/docs/getting-started/composing-components
Composing Components | cva
Class Variance Authority
cva.style
https://xionwcfm.tistory.com/322
tailwind-merge 사용법을 익히고 클래스 병합하기
https://github.com/dcastil/tailwind-merge GitHub - dcastil/tailwind-merge: Merge Tailwind CSS classes without style conflicts Merge Tailwind CSS classes without style conflicts - GitHub - dcastil/tailwind-merge: Merge Tailwind CSS classes without style con
xionwcfm.tistory.com
https://vincentdusautoir.com/posts/button-variants-tailwindcss
Button variants and TailwindCSS - Vincent Dusautoir
How I implement Button variants with TailwindCSS in my projects.
vincentdusautoir.com
https://github.com/nextui-org/tailwind-variants
요즘 cva 대신 눈여겨 보고 있는 라이브러리이다.
tailwind-variants 약간의 open issue들이 좀 치명적인 것 같다. extends, responsive 기능(이거 때문에 눈여겨 봄)을 제공한다.
근데 responsive가 제대로 되지 않는다는 issue가 보인다 😅. 나중에 이슈가 해결되면 사용해 볼 만하다.
GitHub - nextui-org/tailwind-variants: 🦄 Tailwindcss first-class variant API
🦄 Tailwindcss first-class variant API. Contribute to nextui-org/tailwind-variants development by creating an account on GitHub.
github.com