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…

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:

 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…

 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:

 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.

 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…

ReactNotes
Node.jsJavaScript runtime. JavaScript outside the browser.
ReactFront-end JS library. Focus is on the user interface by building “components”; and rendering to the DOM via a “virtual DOM”.
Next.jsA React framework for web sites. Supports server-side rendering for some/all components; routing, etc. for a full framework.
TypeScriptJavaScript, but strongly-typed. Transpiles to plain JS.
JSXA 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.
TSXA syntax extension for TypeScript files to support JSX.
PrismaA database toolkit. It includes a JavaScript/TypeScript ORM for Node.js.
CSS ThingsNotes
Tailwind CSSCSS framework using utility classes. But can get verbose. See daisyUI.
CSS ModulesA CSS Module is a CSS file where all class names and animation names are scoped locally by default.
SassCSS extension language (nesting, mixins, inheritance, etc).
daisyUIAdds component class names to Tailwind CSS - makes it less verbose.
Chakra UICSS component library for use in React applications.
Foundation FrameworkSASS/CSS framework for web UI components.
ToolsNotes
PrettierCode formatter for JS, CSS, HTML, Markdown, and more.
GulpBuild/automation tool on Node.js.
ViteBuild/automation tool; built-in dev server.
GruntBuild/automation tool using JavaScript.
YarnJavaScript package manager.
npmJavaScript package manager.
pnpmJavaScript package manager.
BabelA JavaScript compiler mainly used to convert ECMAScript 2015+ code into backwards compatible versions of JavaScript to match what browsers may support.
WebpackJavaScript module bundler. Takes modules with dependencies and generates static assets representing those modules.
Also…Notes
AngularTypeScript-based single-page web application framework for Node.js.
VueFront end JavaScript framework for building user interfaces and single-page applications.
ExpressBack 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.