Using DaisyUI Components with Alpine.js

14 Aug 2024

Introduction

If you use Tailwind, you may have also encountered DaisyUI and its library of web components built on top of Tailwind.

Maybe, like me, you didn’t realize at first that Daisy has no JS - all its effects are pure CSS.

But there are times when you need some extra help to make the best use of some Daisy components.

Here is one basic example: the tabs component - specifically, in this example, Daisy’s boxed tabs.

Out of the box (no pun intended), if you use the following sample code…

HTML
1
2
3
4
5
<div role="tablist" class="tabs tabs-boxed">
    <a role="tab" class="tab">Tab 1</a>
    <a role="tab" class="tab tab-active">Tab 2</a>
    <a role="tab" class="tab">Tab 3</a>
</div>

…then you get a nice row of tab buttons which don’t do anything. They are completely inert. And they stretch across the entire width of the web page:

It’s not a great amount of effort to add some JavaScript to this, to breathe life into these tabs, but I wanted to try out Alpine.js to see if that could minimize the amount of code I needed to write.

First Attempt to Add Behavior

My first attempt was this:

HTML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<div x-data="{ foo: true, bar: false, baz: false }">
	<div role="tablist" class="tabs tabs-boxed justify-start">
		<a role="tab" class="tab"
		   @click="foo=true; bar=false; baz=false;"
		   :class="foo && 'tab-active'">Foo Tab</a>
		<a role="tab" class="tab"
		   @click="foo=false; bar=true; baz=false;"
		   :class="bar && 'tab-active'">Bar Tab</a>
		<a role="tab" class="tab"
		   @click="foo=false; bar=false; baz=true;"
		   :class="baz && 'tab-active'">Baz Tab</a>
	</div>  
	<div>  
		<div x-show="foo">content one one one</div>
		<div x-show="bar">content two</div>
		<div x-show="baz">content three</div>
	</div>
</div>

This creates three left-justified tab buttons, each of which can open a related content panel:

How does this work?

The Tailwind class justify-start positions the tabs on the left hand side of the tabs bar.

The following Alpine directives are used (in my case I added a CDN <script> to my web page for Alpine):

  • x-data which contains a JavaScript object { foo: true, bar: false, baz: false }. The object is used to control which tab is selected and visible (true). All the other tabs are closed (false). This data is available to all descendant directives.
  • x-on:click - but here I use the shorthand syntax @click. For each tab, there is a click event which resets the x-data object’s values as needed.
  • x-bind:class - but here I use another shorthand: :class. This references a JavaScript expression, which adds the Daisy class tab-active to the element (or removes it), depending on how the JavaScript expression is evaluated - for example, foo && 'tab-active'.
  • x-show which controls which of the tab panels is displayed (the one where the x-data is true).

That is everything needed to create a functioning set of Daisy tabs.

One additional clarification: The JavaScript foo && 'tab-active' may be less familiar to some people (it was to me). These are equivalent expressions here:

`foo ? 'tab-active' : ''`
`foo && 'tab-active'`

Second Attempt to Add Behavior

My first attempt works well, but it involves a lot of repetitious settings of true and false values.

This next attempt tries to improve the situation - at least somewhat…

HTML
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<div x-data="mytabs">
	<div role="tablist" class="tabs tabs-boxed justify-start">
		<a role="tab" class="tab"
		   @click="reveal('foo')"
		   :class="foo && 'tab-active'">Foo Tab</a>
		<a role="tab" class="tab"
		   @click="reveal('bar')"
		   :class="bar && 'tab-active'">Bar Tab</a>
		<a role="tab" class="tab"
		   @click="reveal('baz')"
		   :class="baz && 'tab-active'">Baz Tab</a>
	</div>  
	<div>  
		<div x-show="foo">content one one one</div>
		<div x-show="bar">content two</div>
		<div x-show="baz">content three</div>
	</div>
</div>

And now we also have some separate JavaScript, in a <script> tag:

JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
document.addEventListener('alpine:init', () => {
	Alpine.data('mytabs', () => ({
		// set the selected tab to true:
		foo: true,
		bar: false,
		baz: false,

		reveal(selected_tab) {
			// hide all tab panels:
			this.foo = false;
			this.bar = false;
			this.baz = false;
			// show the selected panel:
			this[selected_tab] = true;
		}
	}))
})

The heart of this approach is the Alpine.data(...) global function, which is registered as an event listener when Alpine is first initialized.

Here mytabs is a JavaScript function, which is referenced in the HTML <div x-data="mytabs">. This means I have basically moved the previous x-data="{...}" settings from their old HTML location into JavaScript.

As well as this x-data, there is also another function reveal(selected_tab) which is called by each @click event. This function resets all tabs to false (not visible) and then finally sets the clicked tab to true (visible).

Note how the code uses this.foo and so on to refer to each data item.

The end result is somewhat less cluttered HTML. I think it is an improvement.

One More Optional Enhancement - Bookmarking

One advantage of the updated approach is that is makes further customizations incrementally easier.

The following modifications allow the tabs page to be opened to any specific tab by appending ?tab=whatever to the end of the page URL as a query string. Now, the whatever tab will be opened by default.

The code also modifies the URL’s query string dynamically, as different tabs are opened - which means you can bookmark specific tabs in the web page.

You can read more about URLSearchParams.

JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
document.addEventListener('alpine:init', () => {
	const tabs = ['foo', 'bar', 'baz'];
	const defaultTab = 'foo';
	// handle any relevant URL query param (?tab=xyz):
	const paramsString = window.location.search;
	const searchParams = new URLSearchParams(paramsString);
	const tabParam = searchParams.get('tab');
	const selectedTab = tabs.includes(tabParam) ? tabParam : defaultTab;

	Alpine.data('mytabs', () => ({
		// set the selected tab to true:
		foo: selectedTab === 'foo',
		bar: selectedTab === 'bar',
		baz: selectedTab === 'baz',

		reveal(selected_tab) {
			// hide all tab panels:
			this.foo = false;
			this.bar = false;
			this.baz = false;
			// show the selected panel:
			this[selected_tab] = true;
			// reset the URL's query param (?tab=xyz):
			if (searchParams.has('tab')) {
				searchParams.delete('tab');
			}
			if (selectedTab !== defaultTab) {
				searchParams.append('tab', selected_tab);
			}
			window.location.search = searchParams.toString();
		}
	}))
})

A Rabbit Hole

Not exactly relevant to the above notes, but just for fun… I discovered Tailwind, Daisy and Alpine when I was investigating Next.js.

Along the way I encountered a plethora of tools and libraries which I had never or rarely used before…

React Notes
Node.js JavaScript runtime. JavaScript outside the browser.
React Front-end JS library. Focus is on the user interface by building “components”; and rendering to the DOM via a “virtual DOM”.
Next.js A React framework for web sites. Supports server-side rendering for some/all components; routing, etc. for a full framework.
TypeScript JavaScript, but strongly-typed. Transpiles to plain JS.
JSX A React syntax extension for JavaScript that lets you write HTML-like markup inside a JavaScript file. See also the spec. Transpiles to plain JS, with the HTML parts transpiled to the relevant React calls, such as React.createElement.
TSX A syntax extension for TypeScript files to support JSX.
Prisma A database toolkit. It includes a JavaScript/TypeScript ORM for Node.js.
CSS Things Notes
Tailwind CSS CSS framework using utility classes. But can get verbose. See daisyUI.
CSS Modules A CSS Module is a CSS file where all class names and animation names are scoped locally by default.
Sass CSS extension language (nesting, mixins, inheritance, etc).
daisyUI Adds component class names to Tailwind CSS - makes it less verbose.
Chakra UI CSS component library for use in React applications.
Foundation Framework SASS/CSS framework for web UI components.
Tools Notes
Prettier Code formatter for JS, CSS, HTML, Markdown, and more.
Gulp Build/automation tool on Node.js.
Vite Build/automation tool; built-in dev server.
Grunt Build/automation tool using JavaScript.
Yarn JavaScript package manager.
npm JavaScript package manager.
pnpm JavaScript package manager.
Babel A JavaScript compiler mainly used to convert ECMAScript 2015+ code into backwards compatible versions of JavaScript to match what browsers may support.
Webpack JavaScript module bundler. Takes modules with dependencies and generates static assets representing those modules.
Also… Notes
Angular TypeScript-based single-page web application framework for Node.js.
Vue Front end JavaScript framework for building user interfaces and single-page applications.
Express Back end web application framework for building RESTful APIs with Node.js.

And the above list is really just scratching the surface of an enormous ecosystem, with no hint of Java anywhere.