We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide12 min read

daisyUI, Tailwind without the wall of classes

daisyUI adds named component classes and 35 themes to Tailwind CSS. CSS configuration, custom themes, React integration, limitations, and a shadcn comparison.

daisyUI, Tailwind without the wall of classes

Tailwind solved the problem of naming classes and created a new one along the way: a button described by twelve utility classes looks like a malfunction in the code. daisyUI adds a layer of component names, so that same button becomes btn btn-primary, with the same Tailwind underneath.

It is a plugin, not a framework. You get no React components and no logic, only CSS classes, so it works identically in Next.js, Vue, Svelte, and plain HTML.

Installation and CSS configuration

Version five dropped the JavaScript configuration file in favour of configuration directly in the stylesheet, following the direction Tailwind itself took.

Code
Bash
pnpm add -D daisyui
Code
CSS
@import "tailwindcss";
@plugin "daisyui" {
  themes: light --default, dark --prefersdark, cupcake;
}

That change simplifies more than it appears to. Themes, colours, and variables live in one file alongside the rest of the styles rather than in a separate configuration, so there is no consistency to maintain between two places.

The marker beside a theme name states its role. One theme is the default, another engages under a system dark setting. The rest stay available for manual switching.

Components and modifiers

The component name is a base class and appearance changes through modifiers. The layout is predictable, and after a few components you guess names correctly.

Code
HTML
<button class="btn btn-primary">Save</button>
<button class="btn btn-primary btn-soft">Cancel</button>
<button class="btn btn-error btn-outline btn-sm">Delete</button>

<div class="card bg-base-100 shadow-sm">
  <div class="card-body">
    <h2 class="card-title">Title</h2>
    <p>Card content.</p>
  </div>
</div>

Version five added soft and dashed styles to buttons, badges, and alerts, so the variant set covers most needs without writing your own. Default heights on buttons, inputs, and toggles were reduced, bringing the look closer to current interfaces, though migrating from an older version calls for a pass over forms.

Every modifier is responsive, so lg:btn-lg works with no extra configuration. That detail removes the most common reason for writing custom classes.

An important rule: Tailwind classes take precedence, so btn bg-red-500 overrides the component colour. You need not choose between the two; you treat components as a starting point and tune them with utility classes.

Themes, the biggest advantage

The library ships thirty five ready themes, and switching means changing one attribute on a parent element.

Code
HTML
<html data-theme="dark">

That works because components reference semantic names rather than specific colours: primary, secondary, accent, base, and content on base. A theme defines values for those names, and components change appearance without touching markup.

You define a custom theme in the same CSS file, giving colours in any format. Tailwind version four moved to CSS variables and a colour mixing function, so there is no value transformation and no need to supply colours in a particular space.

Code
CSS
@plugin "daisyui/theme" {
  name: "company";
  default: true;
  --color-primary: oklch(55% 0.2 250);
  --color-primary-content: oklch(98% 0.01 250);
  --color-base-100: oklch(98% 0 0);
  --color-base-content: oklch(21% 0.01 250);
  --radius-box: 0.5rem;
}

Colour pairs matter practically here. Every background colour has a content colour to go on it, which keeps text on a button readable after any palette change. There is a catch that trips up most guides copied from version four, though: version five removed the automatic calculation of a content colour from its background colour. You supply --color-primary-content and --color-base-content yourself, because nothing computes them for a new theme.

Tuning a built in theme works differently. Give the block the name of an existing theme and change two colours in it, and every other value is inherited from the original, so the pairs keep their contrast without listing the whole set. A theme from scratch means the full set of variables; adjusting a shipped one takes a few lines.

Theme switching in practice

The attribute alone is not enough, since the choice must persist and the first render must not flash the wrong theme. That second problem is the most reported and has one answer: set the attribute before the page paints.

Code
HTML
<script>
  const saved = localStorage.getItem('theme')
  const dark = window.matchMedia('(prefers-color-scheme: dark)').matches
  document.documentElement.dataset.theme = saved ?? (dark ? 'dark' : 'light')
</script>

That script belongs in the document head and must run synchronously. Setting the theme in a React effect is too late, since the browser paints the page in the default theme first and the user sees a flash.

Server rendering adds a second aspect: the server does not know the user's preference, so the markup sent from the server and the markup after hydration must agree. The standard way out is setting the attribute by script before hydration and keeping the theme out of component state.

The switcher itself is an ordinary button storing the choice and changing the attribute. Offer three options rather than two, light, dark, and follow system, since some users expect the application to track the device setting.

Variables worth knowing

Beyond colours a theme sets several variables governing interface shape, and changing them delivers the most for the least effort.

Corner radius is separate for inputs, buttons, and containers, so you can build an interface with sharp cards and round buttons without writing custom classes. A size scale lets you change the density of the whole interface with one value, which panels heavy with data often need.

Border thickness and the press effect are the other two variables worth attention. Set once in the theme they apply everywhere, so there is no returning to individual components.

Practical advice: change these values in the theme, never in components. A handful of overrides scattered across a project is exactly the state the theme system was meant to prevent, and it returns at the first request for a second theme.

Working with React

Since these are plain CSS classes, you write React components yourself and wrap the classes in them. Some count that a drawback and some an advantage, depending on what you are after.

Code
TypeScript
type Props = {
  variant?: 'primary' | 'ghost' | 'error'
  size?: 'sm' | 'md' | 'lg'
} & React.ButtonHTMLAttributes<HTMLButtonElement>

export function Button({ variant = 'primary', size = 'md', className, ...rest }: Props) {
  return (
    <button
      className={`btn btn-${variant} btn-${size} ${className ?? ''}`}
      {...rest}
    />
  )
}

One trap deserves attention. Tailwind scans code for complete class names, so a string assembled from fragments may not reach the output stylesheet. Listing full names in a map is safer than gluing them from pieces.

The second matter is accessibility. Classes give appearance rather than behaviour, so a modal, a dropdown, and tabs need keyboard handling and attributes from you. Some components rest on native HTML elements, which helps but does not excuse you from testing.

Forms and interactive elements

Forms are where the library saves the most code, since fields, labels, and validation messages come with ready classes at consistent sizes.

Code
HTML
<fieldset class="fieldset">
  <legend class="fieldset-legend">Email address</legend>
  <input type="email" class="input w-full" placeholder="jan@example.com" required />
  <p class="label">We will use it only to confirm your order.</p>
</fieldset>

Note that the library rests on native browser mechanisms wherever it can. Validation uses HTML attributes, so a message about an invalid address appears without JavaScript, and field state reflects in styles automatically.

The same approach shows in dropdowns and modals, where native elements replace hidden fields and script toggled classes. The gain is double: less code and better default behaviour under keyboard navigation.

The limit of that approach sits at components the browser lacks. A field with suggestions, a multilevel menu, or a sortable table need JavaScript, and you supply it. The library gives appearance; you assemble the rest yourself or take it from a separate behaviour library.

Output stylesheet size

A common question is what this layer costs in kilobytes. The answer is favourable, since only classes actually used in the code get generated, exactly as in Tailwind itself.

Themes behave differently: every enabled theme adds a set of variables to the stylesheet whether or not anybody uses it. This one is measurable, since the library publishes a stylesheet with the full set. All thirty five weigh 38 kB of minified CSS and 6.4 kB after gzip, roughly a kilobyte each. Turning the whole set on in a project that uses two therefore adds around thirty six kilobytes in the source and a few kilobytes over the wire.

Sensible practice is listing only the themes actually offered in the interface. If the application has light and dark, name two and leave the rest disabled. The others are worth enabling while you work on the look, since they make a good reference when picking your own palette.

daisyUI against the alternatives

ToolStrengthWeaknessPick it when
daisyUIThemes, short markup, works in any frameworkNo logic or accessibility includedFast project, many themes, any framework
shadcn/uiComponent code lives with you, accessibility includedReact only, more code to maintainReact application with accessibility requirements
HeroUIReady components with behaviour and animationTied to the libraryProject where time to ship matters most
FlowbiteLarge set of ready blocksSome content is paidAdmin panels and marketing pages

The core difference against shadcn lies in where component code lives. Here a component is a CSS class from the library, so updates arrive with the package. There you copy the component into the project and from then on it is yours, maintenance included.

The phrase "works in any framework" needs qualifying, since it sounds stronger than it means. The classes know nothing about a framework, so the same names paste into Svelte, into Vue, and into SolidJS, where a component runs once and the reactivity model resembles nothing in React. What you do not get is components to import, so wrapping the classes and all keyboard handling falls to you separately in each of those places. The advantage is that the appearance layer survives a change of framework, not that there is less work.

The choice depends on whether you need control over detail. For a panel where pace and consistency matter, library classes win. For a product with its own design system and accessibility requirements you will arrive at your own components sooner or later anyway.

Consistency in a larger team

A class library solves appearance and not discipline. In a project several people work on, the same card will eventually exist in three variants, since everyone adds their own utility classes.

The simplest safeguard is wrapping the most common layouts in your own components and treating library classes as an implementation detail. Then changing card appearance across the application is one place rather than a repository wide search.

The second is a code review rule: no new hardcoded colour classes. Colour comes from the theme or from a semantic name rather than from the Tailwind palette, since otherwise a second theme stops working across half the interface.

The third is a list of components you use, written down in the repository. The library offers several dozen options where a project needs a dozen or so, and the rest is room for pointless variety.

When it is the wrong choice

A project with an established design system and component library gains nothing and adds another layer of classes mixing with the existing ones.

An application with high accessibility requirements needs components with keyboard and screen reader support included. Classes alone do not provide that, so you write it or take a behaviour library.

The third case is a team not using Tailwind. The plugin extends it, so without it there is nothing to discuss.

The fourth is a product where appearance is a differentiator. Ready made components speed up the work and at the same time make an interface resemble hundreds of others built from the same set. For an internal panel that does not matter; for a sales page it sometimes does, though a custom theme with a different palette and a different corner radius shifts perception more than you might expect.

Common mistakes

The first is assembling class names from fragments. Tailwind will not find such a class when scanning and the style will not reach the output, with the error surfacing only in the production build.

The second is overriding component colours with classes rather than changing the theme. The effect is the same on one page, but switching themes breaks everything, since hardcoded values do not respond.

The third is assuming a component is accessible. A modal without a focus trap and without escape key handling looks fine and fails for some users.

The fourth is mixing several component libraries at once. Two class systems in one project lead to specificity conflicts nobody wants to untangle later.

The fifth is migrating from an older version without reviewing forms. The default size change in version five is slight on one element and visible in a dense form.

FAQ

Does daisyUI work with Tailwind CSS 4?

Yes, version five of the library targets Tailwind version four specifically. Configuration moved from a JavaScript file into the stylesheet, and colours use CSS variables, so you may supply them in any format.

daisyUI or shadcn/ui?

Choose daisyUI when you want ready classes, many themes, and work in any framework. Choose shadcn/ui when you work in React, need accessibility included, and want component code in your own repository.

Are the components screen reader accessible?

Partly, since some rest on native HTML elements. The library supplies appearance rather than behaviour, though, so focus trapping, keyboard handling, and helper attributes need adding yourself or taking from a behaviour library.

How many themes ship with it?

Thirty five ready ones, plus the option to define your own in the same CSS file. Switching means changing one attribute, and components respond automatically, since they reference semantic names rather than specific colours.

Does it slow a site down?

No, it is ordinary CSS generated alongside Tailwind, and unused classes never reach the output. The plugin adds no JavaScript, so the browser side bundle does not grow.

Documentation sits on the project site, and the version five changes appear in the release notes.