Build a Dark Mode Theme Toggle with Qwik and Tailwind CSS, Part 1

In this tutorial, we'll create a dark mode theme toggle component using Qwik and Tailwind CSS. This component will provide a dropdown menu that allows users to switch between light and dark themes, or choose the system default preference.

The Qwik website provides a starting point for theme management, but I found it to be lacking in detail for my requirements. I have taken inspiration from their example, but have expanded on it to provide a more complete solution.

This post got a bit lengthy so I've split it into two parts. First we will set up the project and learn a bit about Qwik and Tailwind as we create a basic webpage with both light and dark mode styling. Then, in Part Two, we will be creating the dropdown dark mode selector and incorporating it into the website.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setting Up the Project
  4. Adding Styles with Tailwind
  5. Conclusion

Introduction

Qwik

Qwik is a frontend framework that is optimized for instant loading and startup performance. It is similar to frameworks like Next, Nuxt and SvelteKit, in that it provides a component-based web development platform incorporating server-side rendering to quickly get content on a user's screen. But it is unlike these frameworks in that under the hood it relies on resumability and incremental JavaScript loading to not only get content to the screen quickly, but to provide instant interactivity.

Other frameworks like those mentioned above rely on hydration. This essentially means that an app is run twice: once on the server to generate the initial HTML, and once on the client to make the app interactive. Qwik, on the other hand, runs the app only once. It runs on the server at startup to generate the HTML and initial state, then embeds the state into the HTML and sends it to the client. The client then uses the embedded information to resume the app where the server left off. It also provides information about where to fetch JavaScript bundles for specific components, so that only the necessary JavaScript is loaded, and only when needed. You can find much more information about how Qwik works in their official documentation.

In addition to having great performance, Qwik is easy to use. If you are familiar with React, you should feel right at home using Qwik. While it is a little different, many of the concepts are the same. Including the use of functional components, hooks, and JSX as an HTML template syntax.

Tailwind

Tailwind CSS is a utility-first CSS framework that feels somewhat like Bootstrap, but without pre-styled components like btn, and with optimizations that only ship the CSS you actually use. It is highly customizable and makes things like responsive design or - importantly for this tutorial - dark mode, very easy to implement.

That said, Tailwind is not for everyone. Some of the more common issues I've heard people have is that it combines styling with markup - breaking separation of concerns - that it leads to long class names - impacting readability - and that it can be repetitive in some situations.

These are fair criticisms, but I find them to be minor issues, and minimized when you break down components enough to eliminate repetition of markup. If you really don't like it, you can substitute any other method of styling you prefer. Qwik supports many options. But it'll be harder to follow along here.

Prerequisites

Before we get started, make sure you have the following installed:

  • Node.js
  • A package manager

I will be using pnpm for my package manager in this tutorial.

Setting Up the Project

First, create a new Qwik project with the Qwik CLI, which can be accessed through the package manager. Run the following command in your terminal:

pnpm create qwik@latest

You'll be prompted to choose a directory to install the project files and a starter template. Use any directory you prefer, I'll use ./qwik-dark-mode. For the template, use the "Empty App". Select "Yes" to install pnpm dependencies and to initialize a new git repository.

Navigate to the directory and open it in your code editor:

cd qwik-dark-mode

if using VS Code:

code .

Familiarize yourself with the project structure and files. See the Qwik documentation for a detailed rundown.

Adding Styles with Tailwind

Adding Tailwind

Qwik has a number of pre-configured integrations with popular libraries and tools. One of them being Tailwind. Simply run

pnpm run qwik add tailwind

This will automatically add the dependency and update required configuration files. It will even add the tailwind prettier plugin so that tailwind classes maintain a consistent ordering throughout the project. This is a nice touch from the Qwik team, and very helpful for keeping Tailwind projects maintainable.

Creating the web page

For this tutorial we'll be keeping our main webpage very simple. We'll only have one route, the home page. Let's get that set up, starting with the markup and layout.

Find the src/routes/layout.tsx file. In Qwik, layouts provide nested UI and request handling (middleware) to a set of routes (docs). In the default component you will see an example of request handling with the onGet function that controls caching. We won't be using request handling in this tutorial, but it is a feature of Qwik that is important to understand for larger projects.

The content returned in layout files is static for all sub-routes using it. This is the root layout so will contain common code for the whole website. We don't strictly need to use it for such a simple website, but to demonstrate how it is used, lets add the nav bar here.

Replace the contents of the file with the following:

import { component$, Slot } from "@builder.io/qwik"
import {
  Link,
  useDocumentHead,
  type RequestHandler,
} from "@builder.io/qwik-city"

// Default `onGet` here. We don't need it for now.

export default component$(() => {
  const head = useDocumentHead()
  return (
    <>
      <header>
        <h1>{head.title}</h1>
        <nav>
          <ul>
            <li>
              <Link href="#home">Home</Link>
            </li>
            <li>
              <Link href="#about">About</Link>
            </li>
          </ul>
        </nav>
      </header>
      <Slot />
    </>
  )
})

A few things of note here:

  • useDocumentHead is a hook that allows you to access data in the HTML document head. We use it here to dynamically set the primary heading of the page. We'll see how to set this data in a page shortly. (docs)
  • Slot is used to nest content within the layout. Think of slots like children in React components. These can be used in any component, but in layouts they display the content of the sibling index.tsx file, or the main content of the page. (docs)
  • Link is a way to navigate in Qwik. It is basically a souped-up anchor tag. You can also use standard anchor tags, but Link provides a few extra features like prefetching and preloading. A Link also triggers single-page navigation within the website. If you prefer full-page navigation and a multi-page app, you can do that by using standard anchor tags. (docs)

Now lets create some content to display in the layout. Open the src/routes/index.tsx file and replace the contents with the following code. Normally we would break up these sections into smaller components, but we'll keep it simple for now.

import { component$ } from "@builder.io/qwik"
import type { DocumentHead } from "@builder.io/qwik-city"

export default component$(() => {
  return (
    <>
      <main>
        <article>
          <section>
            <h2>Introduction</h2>
            <p>
              This is a paragraph of text that provides an introduction to the
              website. Here, you can use the <strong>strong</strong> tag to
              emphasize text and the <em>em</em> tag to italicize text.
            </p>
          </section>

          <section>
            <h3>Subheading</h3>
            <p>
              Here is another paragraph under a subheading. You can also include
              a <a href="https://qwik.dev/">link</a> to an external website.
            </p>
          </section>

          <section>
            <h3>Lists</h3>
            <ul>
              <li>Unordered list item 1</li>
              <li>Unordered list item 2</li>
              <li>Unordered list item 3</li>
            </ul>
            <ol>
              <li>Ordered list item 1</li>
              <li>Ordered list item 2</li>
              <li>Ordered list item 3</li>
            </ol>
          </section>
        </article>
      </main>

      <footer>
        <p>&copy; 2024 My Website. All rights reserved.</p>
      </footer>
    </>
  )
})

export const head: DocumentHead = {
  title: "Example Website With a Theme Toggle",
  meta: [
    {
      name: "description",
      content: "A website to demonstrate a theme toggle",
    },
  ],
}

This is all standard HTML. The only thing to note is the head export. This is how you set data in the document head for a page and it is where we are populating the h1 heading in the layout. You can set the title, meta tags, and other data here. This data is accessible in the layout using the useDocumentHead hook.

You can now run pnpm start to view the website in your browser. You should see a very simple website with the Tailwind browser reset styles.

Initial website with base Tailwind styles
Initial website with base Tailwind styles

Adding Basic Styles

Now let's see some Tailwind magic! We are going to use the Tailwind Typography plugin to transform this page instantly. Run the following command to install the plugin:

pnpm install -D @tailwindcss/typography

Then add it to the tailwind.config.js file in the plugins array:

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
  theme: {
    extend: {},
  },
  plugins: [require("@tailwindcss/typography")],
}

Now all we have to do to add nice styling to the content of the page is to add the prose class to the article tag in routes/index.tsx.

<article class="prose">...</article>
Article content with Tailwind prose class
Article content with Tailwind prose class

This is not necessary something you would want to add to all the content of every webpage, but it provides great defaults for text content.

Before getting to dark mode styling, let's make sure we have a basic nav bar to work with as well. Update the returned content in routes/layout.tsx to include the following styles (or get creative and include your own styling). It should look something like this when done.

Styled navigation bar and title
Styled navigation bar and title
<>
  <header>
    <h1 class="mt-16 text-3xl font-bold">{head.title}</h1>
    <nav class="fixed left-0 top-0 w-full bg-white">
      <ul class="flex">
        <li class="px-4 py-3">
          <Link href="#home">Home</Link>
        </li>
        <li class="px-4 py-3">
          <Link href="#About">About</Link>
        </li>
      </ul>
    </nav>
  </header>
  <Slot />
</>

Let's walk through this line by line. Tailwind has great documentation, so if you want additional information on any of these classes, head over to their website.

  • We first give the h1 a large top margin to accommodate the fixed nav bar. In Tailwind, multiples of 4 in classes like this represent 1 rem, so mt-16 is margin-top: 4rem. We also make the text large and bold.
  • We then position the nav bar as fixed to the top and left of the screen with a full width and a white background.
  • To align the nav items horizontally, we make the ul display as flex.
  • To give nav bar some height, and to increase the clickable surface of the nav items, we give each item padding on the x and y axis separately. There is some repetition here, but these can be extracted into a component to reduce that.

I have also added 1rem of horizontal padding to the body element in src/root.tsx to push the content of the page out from the very edge of the screen. I'll leave this as a learning exercise to the reader. If you're feeling up to it, try to give the content a max width and center it in the page as well.

Can you use Tailwind to make these styles responsive at various breakpoints? Remember, the docs are your best friend.

Adding Dark Styles

Ok, we have a basic page, and now we are ready to add some dark mode styling. We'll start by providing some default styles to the body, give the nav bar a dark background, and then adding dark styles to the prose section.

In src/root.tsx:

<body class="bg-zinc-50 text-zinc-950 dark:bg-zinc-900 dark:text-zinc-100">
  ...
</body>

in src/routes/layout.tsx:

<nav class="bg-zinc-100/80 backdrop-blur-md dark:bg-zinc-800/80">...</nav>

and in src/routes/index.tsx simply add dark:prose-invert:

<article class="prose dark:prose-invert">...</article>

Believe it or not, we now have working dark mode! Just not a good way to let users switch between them. We'll see that shortly, but first let's break down what we've done here.

To add dark styles to any element, you simply add the dark: prefix before the class name. By default this checks the prefers-color-scheme media query to determine which styles to use. Above we are setting background and text colors using one of Tailwind's default colors. The number following the color name indicates the shade of the color with low numbers being lighter and high numbers darker. The /80 in the background color is a shorthand for setting the opacity of the color, in this case to 80%.

The backdrop-blur-* class is a Tailwind class that applies a backdrop blur to the element. This is a CSS property that blurs the content behind the element, and is a great way to add a frosted glass effect to elements. The md is one of several options to set the amount of blur you prefer. Try playing around with different opacity levels and blurs to see how these styles interact. If you narrow your browser window, you should see the effect in action as you scroll past content.

To view the dark mode styles, you can open your browser dev tools and toggle the dark mode in the settings under the Elements tab. In Styles there should be a button to let you test dark mode preference. In Chrome, this looks like a paintbrush icon.

Website with dark mode
Website with dark mode

This is great, but we need a way to programmatically select a theme preference from within the website.

Tailwind provides two ways to determine if dark mode is enabled. The first is the default behavior we see here. The second is to use the class strategy. This makes it so that dark mode will not trigger on the media query, but on the presence of a dark class on a parent element. Typically this is done on the html element. In order to allow users to select a dark mode, we'll need to use this strategy.

Note that in the docs, the newest version uses selector instead of class, but I found class to work with my code. If you have issues, try using selector instead.

Update the tailwind.config.js file:

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: "class",
  ...
}

To test this out, try adding the dark class to the body element in root.tsx. We do not have access to the html element in a Qwik project, but you can add the class there programmatically by running the following in your browser console (remove it with the remove method):

document.documentElement.classList.add("dark")

This forms the basis of how we will be switching between light and dark modes with the theme toggle component we'll create next time.

Conclusion

We now have the foundation in place required to create a dark mode theme toggle. In the next part of this tutorial, we will create the dropdown menu that allows users to select their preferred theme. We will also learn how to set an initial theme during page load.

Head over to Part 2 to get started!