Integrating HeadlessUI and TailwindCSS with Nuxt.js: A Comprehensive Guide

Learn how to integrate HeadlessUI and TailwindCSS with Nuxt.js applications in this comprehensive, step-by-step guide.

Mostafa Said
Nuxt 3

Mastering Nuxt 3 course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view the Nuxt 3 course

Headless UI is a set of completely unstyled, accessible UI components designed to smoothly integrate with utility-first CSS frameworks like Tailwind CSS. If you're working with Nuxt.js and want to leverage these components, this guide will walk you through the process step-by-step, ensuring you can effortlessly integrate HeadlessUI into your Nuxt.js projects.

Whether you're aiming for improved accessibility, a streamlined development workflow, or optimized performance, this tutorial covers all the essentials for using HeadlessUI and TailwindCSS in your Nuxt.js applications. Additionally, by the end of this article, we will explore alternatives to Headless UI to help you make an informed decision on which UI library you should choose for your Nuxt js or Vue js project.

Introduction to Headless UI

HeadlessUI Logo with dark background

HeadlessUI is a library of unstyled, accessible UI components that can be customized to fit any design system or style guide. Unlike many UI libraries that come with pre-defined styles, Headless UI focuses solely on functionality, leaving the styling entirely up to you. This allows developers to have full control over the appearance of their application, ensuring it adheres to the desired design standards.

Why Use Headless UI?

With many UI libraries available in the Vue.js and Nuxt.js ecosystem, why choose Headless UI? Here are some of its key advantages:

1. Customizability

Headless UI components are completely unstyled, which means you can apply your own styles using any CSS framework or custom CSS. This is particularly useful for projects with strict design requirements or unique branding needs. You are not constrained by pre-defined styles and can achieve a look that is entirely your own.

2. Integration with Utility-First CSS

Headless UI works perfectly with utility-first CSS frameworks like Tailwind CSS. This allows for rapid development and consistent design across your application. Utility classes make it easy to apply styles directly in your markup, reducing the need for complex stylesheets and improving maintainability. TailwindCSS is super easy to learn and will help you style your components in no time. You can learn TailwindCSS from scratch in the Vue School course Tailwind CSS Fundamentals.

3. Flexibility

While TailwindCSS alone is excellent for styling Headless UI components from scratch, there are other options such as Daisy UI. It provides a set of pre-defined TailwindCSS classes to style components.

Daisy UI can work alongside Headless UI to simplify the process of building beautiful UIs. To find out more about Daisy UI, checkout the Crafting a Custom Component Library with Vue and Daisy UI course available on Vue School.

4. Accessibility

One of the primary benefits of using Headless UI is its focus on accessibility. Each component is built with accessibility in mind, ensuring that your application can be used by people with disabilities. This includes keyboard navigation, screen reader support, and ARIA attributes, making it easier to create inclusive web applications.

Setting Up Nuxt.js with Headless UI

To get started, you'll need a Nuxt.js project. If you don't have one yet, you can create it using the following command:

npx nuxi@latest init my-nuxt-app

Navigate to your project directory:

cd my-nuxt-app

Nuxt.js provides a powerful and flexible framework for building Vue.js applications. Its server-side rendering capabilities and module system make it an excellent choice for integrating additional libraries like Headless UI.

Adding Headless UI to Your Nuxt Project

Step 1: Install the nuxt-headlessui Module

You can install the nuxt-headlessui module using one of the following package managers:

# Using pnpm
pnpm add -D nuxt-headlessui

# Using yarn
yarn add --dev nuxt-headlessui

# Using npm
npm install --save-dev nuxt-headlessui

Step 2: Configure the Module in nuxt.config.ts

Add nuxt-headlessui to the modules section of your nuxt.config.ts file:

export default defineNuxtConfig({
  modules: [
    'nuxt-headlessui'
  ],
}

And you can also change the component prefix

export default defineNuxtConfig({
  modules: [
    'nuxt-headlessui'
  ],
  headlessui: {
       prefix: 'Headless'
  }
}

This configuration step integrates Headless UI into your Nuxt.js project, allowing you to use its components seamlessly. The optional prefix setting lets you customize the prefix used for Headless UI components, which will help in avoiding naming conflicts with other components in the project.

Step 3: Configure SSR-Safe IDs in app.vue

In case you’re using Nuxt with Server-Side Rendering, to avoid hydration issues due to ID mismatches, add the following line to your app.vue file inside <script setup>:

<script setup>
    provideHeadlessUseId(() => useId());
</script>

This ensures that SSR-safe IDs are used for Headless UI components, which is crucial for maintaining consistency between server-rendered and client-rendered content.

Using Headless UI Components

After completing the setup, you can start using Headless UI components in your Nuxt app without needing to import them manually. Here’s how you can use a Listbox component.

Example: Listbox Component

In this example, we will create a Listbox component and style it using plain CSS.

Step 1: Define the Data and State

In your component file, define the data and state required for the Listbox:

<script setup>
    import { ref } from 'vue'
    
    const people = [
      { name: 'Wade Cooper' },
      { name: 'Arlene Mccoy' },
      { name: 'Devon Webb' },
      { name: 'Tom Cook' },
      { name: 'Tanya Fox' },
      { name: 'Hellen Schmidt' }
    ]
    const selectedPerson = ref(people[0])
</script>

In the code above, we use the script setup syntax and the Composition API to define a list of people as an array of objects, each containing a name property. We also create a reactive reference selectedPerson to store the currently selected person, initializing it with the first person in the array. This reference will dynamically update as different people are selected, enabling reactive behavior in our component.

Step 2: Create the Listbox Component

Create the Listbox component using Headless UI components and style it with plain CSS:

<script setup>
    import { ref } from 'vue'
    
    const people = [
      { name: 'Wade Cooper' },
      { name: 'Arlene Mccoy' },
      { name: 'Devon Webb' },
      { name: 'Tom Cook' },
      { name: 'Tanya Fox' },
      { name: 'Hellen Schmidt' }
    ]
    const selectedPerson = ref(people[0])
</script>

<template>
  <div class="listbox-container">
    <HeadlessListbox v-model="selectedPerson">
      <div class="relative">
        <HeadlessListboxButton class="listbox-button">
          <span>{{ selectedPerson.name }}</span>
          <span class="icon">▼</span>
        </HeadlessListboxButton>

        <HeadlessListboxOptions class="listbox-options">
          <HeadlessListboxOption
            v-for="person in people"
            :key="person.name"
            :value="person"
            as="template"
          >
            <li class="listbox-option">
              {{ person.name }}
            </li>
          </HeadlessListboxOption>
        </HeadlessListboxOptions>
      </div>
    </HeadlessListbox>
  </div>
</template>

<style>
.listbox-container {
  width: 300px;
  margin: 0 auto;
}

.listbox-button {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
}

.icon {
  margin-left: 8px;
}

.listbox-options {
  position: absolute;
  z-index: 10;
  width: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: white;
  max-height: 150px;
  overflow-y: auto;
}

.listbox-option {
  padding: 10px;
  cursor: pointer;
}
</style>

This basic example demonstrates how to create and use a Listbox component with plain CSS. The Headless UI components provide the necessary functionality, while you have complete control over the styling. And here’s how the component will look in the browser:

A gif shows the HeadlessUI components in action in Nuxt

And it’s worth mentioning that any component used from Headless UI, the module will auto and dynamically import it. This ensures that you get to spend less time writing import statements while maintaining the a small bundle size.

Enhancing the Project with Tailwind CSS

Tailwind CSS is a utility-first CSS framework that makes it easy to style your components quickly and efficiently. If you want to learn more about TailwindCSS, watch our Tailwind CSS Fundamentals course. Let's add Tailwind CSS to our project and use it to style our Listbox component.

Step 1: Install Tailwind CSS

Install Tailwind CSS and its dependencies:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Or, you can add the TailwindCSS module for Nuxt by using the following command:

npx nuxi@latest module add tailwindcss

This command will install the module’s dependencies and add it to the modules array in nuxt.config.ts . This module will allow you to skip step 2 and 3 since it will configure Tailwind in your Nuxt project automatically.

Step 2: Configure Tailwind CSS

Add the paths to all of your template files in your tailwind.config.js file:

module.exports = {
  content: [
    "./components/**/*.{js,vue,ts}",
    "./layouts/**/*.vue",
    "./pages/**/*.vue",
    "./plugins/**/*.{js,ts}",
    "./app.vue",
    "./error.vue",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Step 3: Add Tailwind CSS to Your Project

Add the following lines to your assets/css/tailwind.css file:

@tailwind base;
@tailwind components;
@tailwind utilities;

Import this file in your nuxt.config.ts:

export default defineNuxtConfig({
  modules: ["nuxt-headlessui"],
  headlessui: {
    prefix: "Headless",
  },
  css: ["~/assets/css/main.css"],
  postcss: {
    plugins: {
      tailwindcss: {},
      autoprefixer: {},
    },
  },
}

Practical Example: Creating a Listbox Component with Tailwind CSS

Now, let's enhance our Listbox component using Tailwind CSS and @heroicons/vue for better styling.

<script setup>
import { ref } from 'vue'
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/20/solid'

const people = [
  { name: 'Wade Cooper' },
  { name: 'Arlene Mccoy' },
  { name: 'Devon Webb' },
  { name: 'Tom Cook' },
  { name: 'Tanya Fox' },
  { name: 'Hellen Schmidt' }
]
const selectedPerson = ref(people[0])
</script>

<template>
  <div class="container mx-auto">
    <div class="w-72">
      <HeadlessListbox v-model="selectedPerson">
        <div class="relative mt-1">
          <HeadlessListboxButton
            class="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm"
          >
            <span class="block truncate">{{ selectedPerson.name }}</span>
            <span
              class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
            >
              <ChevronUpDownIcon
                class="h-5 w-5 text-gray-400"
                aria-hidden="true"
              />
            </span>
          </HeadlessListboxButton>

          <transition
            leave-active-class="transition duration-100 ease-in"
            leave-from-class="opacity-100"
            leave-to-class="opacity-0"
          >
            <HeadlessListboxOptions
              class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
            >
              <HeadlessListboxOption
                v-for="person in people"
                v-slot="{ active, selected }"
                :key="person.name"
                :value="person"
                as="template"
              >
                <li
                  :class="[
                    active ? 'bg-amber-100 text-amber-900' : 'text-gray-900',
                    'relative cursor-default select-none py-2 pl-10 pr-4',
                  ]"
                >
                  <span
                    :class="[
                      selected ? 'font-medium' : 'font-normal',
                      'block truncate',
                    ]"
                    >{{ person.name }}</span
                  >
                  <span
                    v-if="selected"
                    class="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600"
                  >
                    <CheckIcon class="h-5 w-5" aria-hidden="true" />
                  </span>
                </li>
              </HeadlessListboxOption>
            </HeadlessListboxOptions>
          </transition>
        </div>
      </HeadlessListbox>
    </div>
  </div>
</template>

With Tailwind CSS, you can use utility classes to quickly style your components. This approach makes your styles more maintainable and reduces the need for custom CSS.

Here’s how our component looks now:

A gif shows the HeadlessUI components styled with TailwindCSS in Nuxt

A gif shows the HeadlessUI components styled with TailwindCSS in Nuxt

Using Headless UI Plugin for TailwindCSS

Since we’re using Tailwind CSS, we can install the @headlessui/tailwindcss plugin to gain access to built-in modifiers like ui-open:* and ui-active:*. We first need to execute the following command to install the plugin:

npm install @headlessui/tailwindcss

Then, modify the tailwind.config file to add the plugin to the plugins array:

module.exports = {
  content: [
    "./components/**/*.{js,vue,ts}",
    "./layouts/**/*.vue",
    "./pages/**/*.vue",
    "./plugins/**/*.{js,ts}",
    "./app.vue",
    "./error.vue",
  ],
  theme: {
    extend: {},
  },
  plugins: [
      require('@headlessui/tailwindcss'),
  ],
}

With that done, we can now add the following class to the HeadlessListboxOption component to change highlighting colors for the selected, active option:

<HeadlessListboxOption
  v-slot="{ active, selected }"
  v-for="person in people"
  :key="person.name"
  :value="person"
  as="template"
  class="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"
>

Here’s how it looks now:

Screenshot for the HeadlessUI rendered component after utilizing ui-active class.

Exploring Alternatives

While Headless UI provides a great foundation for building accessible and customizable components, there are other options available that might better suit your needs depending on your project's requirements.

RadixUI Vue

One such alternative is Radix Vue. Radix Vue provides a set of high-quality, accessible UI components designed for building modern web applications. These components are unstyled, giving you full control over their appearance while ensuring they meet the highest accessibility standards.

Daisy UI

Daisy UI is a plugin for Tailwind CSS that provides a set of pre-styled, accessible components. It simplifies the process of building beautiful UIs by combining the power of Tailwind CSS with a comprehensive set of components.

If you're interested in learning more about creating custom component libraries using Vue and Daisy UI, consider checking out the Crafting a Custom Component Library with Vue and Daisy UI course available on Vue School. This course provides in-depth knowledge and practical examples to help you build your own component library, offering an excellent complement to the skills you've gained with Headless UI.

Vuetify

Another powerful option is Vuetify. Vuetify is a comprehensive component library for Vue.js that follows the Material Design guidelines. It offers a wide range of pre-designed components that are both visually appealing and functionally robust. Vuetify is ideal for developers looking to build applications with a consistent and polished look without spending too much time on design.

To dive deeper into Vuetify and learn how to leverage its full potential, consider exploring Material UI with Vuetify and Vue.js course available on Vue School.

ShadCN Vue

For those interested in a more minimalistic approach, ShadCN-Vue is an excellent choice. ShadCN provides a simple, unopinionated set of components designed to be highly customizable and easy to integrate with Vue.js projects. This library is perfect for developers who prefer to have more control over both the styling and behavior of their components.

We will be using ShadCN extensively in the Vue.js Master Class 2024 Edition course at Vue School. This course will cover advanced techniques and best practices for building modern web applications with Vue.js, offering an in-depth look at how to make the most of ShadCN in your projects.

Quasar

Additionally, you might want to consider Quasar. Quasar is a high-performance, feature-rich Vue.js framework that provides a unified experience for developing responsive and progressive web applications. It includes a vast set of components and utilities, making it a versatile choice for both web and mobile app development.

Exploring these alternatives can help you choose the right tools for your specific project needs, ensuring you have the best possible foundation for building high-quality Vue.js applications.

Conclusion

Integrating Headless UI with Nuxt.js offers a powerful combination of unstyled, accessible UI components and the robust features of Nuxt.js. By following this guide, you should now have a solid understanding of how to set up and use Headless UI components in your Nuxt.js projects. The flexibility and accessibility of Headless UI, combined with the ease of styling provided by Tailwind CSS, make it an excellent choice for building modern, responsive web applications.

To further enhance your Vue.js skills and knowledge, check out the Vue.js Master Class 2024 Edition course, where we integrate ShadCN-Vue with Vue.js to build a production-ready application. This comprehensive course will provide practical insights and advanced techniques to elevate your Vue.js development expertise.

Mostafa Said
Mostafa is a full-stack developer, a full-time Instructor at Vue School, and a Vue.js Jedi.

Follow MasteringNuxt on