CoreUI is in beta - APIs and components may change.
CoreUI
Utils

cn

Combines classNames with Tailwind v4's class conflict resolution.


Every CoreUI component passes its classes through cn, which is what lets a className you pass in override the component's own styles instead of racing them in the cascade.

Installation

Install the following dependencies:

npm install clsx tailwind-merge

Create a utils/cn.ts file and paste the following code into it.

utils/cn.ts
import clsx, { type ClassValue } from 'clsx';import { extendTailwindMerge } from 'tailwind-merge';export { type ClassValue } from 'clsx';// CoreUI Typography Classes - Dynamic Pattern Matchingconst typographyConfig = {  title: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],  label: ['xl', 'lg', 'md', 'sm', 'xs', '2xs'],  paragraph: ['xl', 'lg', 'md', 'sm', 'xs'],  subheading: ['md', 'sm', 'xs', '2xs'],  doc: ['label', 'paragraph'],  ln: [    'title-h1',    'title-h2',    'title-h3',    'title-h4',    'title-h5',    'title-h6',    'label-lg',    'label-md',    'label-sm',    'label-xs',    'paragraph-lg',    'paragraph-md',    'paragraph-sm',    'paragraph-xs',    'subheading-xs',    'docs-sm',    'special-13-desc',  ],};const typographyPatterns = Object.entries(typographyConfig).flatMap(  ([category, sizes]) => sizes.map((size) => `${category}-${size}`),);export const twMergeConfig = {  extend: {    classGroups: {      'font-size': [        {          text: typographyPatterns,        },      ],    },  },};const customTwMerge = extendTailwindMerge(twMergeConfig);/** * Utilizes `clsx` with `tailwind-merge`, use in cases of possible class conflicts. */export function cn(...classes: ClassValue[]) {  return customTwMerge(clsx(...classes));}

IntelliSense setup (optional)

For class name completion inside cn and tv, add this to your .vscode/settings.json:

.vscode/settings.json
{
  "tailwindCSS.experimental.classRegex": [
    ["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
  ]
}

Prettier setup (optional)

If you use prettier-plugin-tailwindcss, list cn so its classes get sorted too:

prettier.config.mjs
const config = {
  plugins: ['prettier-plugin-tailwindcss'],
  tailwindFunctions: ['cn'],
};

export default config;

Why the custom merge config

tailwind-merge resolves conflicts by knowing which classes belong to the same group — it drops px-2 when it sees a later px-4. It can't know that about CoreUI's typography scale, because text-label-sm and text-paragraph-md are custom utilities, not stock Tailwind ones.

Without the config, both would survive a merge and the winner would come down to source order. twMergeConfig registers every title-*, label-*, paragraph-*, subheading-* and doc-* name as a single font-size group, so the last one wins as you'd expect:

cn('text-label-sm', 'text-paragraph-md'); // → 'text-paragraph-md'

twMergeConfig is exported rather than kept private because tv needs the same group definitions.

The ln-* entries are documentation-only

The ln category in the config covers the tokens this documentation site uses for its own prose. They're harmless to keep, but nothing in components/ui references them — drop that block if you want the file to carry only what your app uses.

Examples

cn takes anything clsx accepts, so conditional objects and arrays work alongside plain strings:

example.tsx
import { cn } from '@/utils/cn';

function MyComponent({
  className,
  isActive,
  ...rest
}: React.HTMLAttributes<HTMLDivElement> & {
  isActive?: boolean;
}) {
  return (
    <div
      className={cn(
        'size-3 bg-bg-white-0 text-text-strong-950',
        {
          'bg-bg-strong-950 text-text-white-0': isActive,
        },
        className,
      )}
      {...rest}
    />
  );
}

Passing className last is the important part — that's the position that lets a caller override the defaults.

On this page