Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Steve 'Cutter' Blades
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Steve 'Cutter' Blades

Creating A CSS Class Name Directive In Alpine.js 3

By
Published in Comments (1)

Alpine.js uses the x-bind:class syntax (or :class shorthand notation) to dynamically apply CSS class names to a given element. This binding can take either a single class name or an inline object that defines multiple class names. This directive provides good developer ergonomics; but, I miss the additional way in which Angular allows individual CSS classes to be bound with separate attributes. Today, I wanted to see if I can recapture some of that Angular magic in a custom Alpine.js directive.

I'm going to call this directive cx (inspired by the commonly-used React module for dynamic class names). Each cx attribute requires a "value" - the token that comes after the (:) in the attribute - to denote a CSS class name; and, an expression to be evaluated as Truthy which determines whether or not said class name is applied to the current element.

For example, if I wanted to conditionally apply the CSS class name, active, based on the scope value, isActive, I could use the following attribute directive:

<div x-cx:active="isActive">

This directive can be used multiple times on a single element (each with a different : value); and, can work alongside the native class attribute and the existing x-bind:class directive variations.

To explore this directive, I've put together a demo in which several different CSS classes can be toggled on a given element using a set of buttons with @click bindings. Each button will toggle a scope property which will, in turn, toggle a CSS class name.

In the following code, all of the dynamic class start with message--. And, the root x-data binding defines the scope properties that drive the dynamic CSS class name attributes.

<!doctype html>
<html lang="en">
<head>
	<style type="text/css">
		button {
			background-color: #f0f0f0 ;
			border: 1px solid #999990 ;
			border-radius: 5px 5px 5px 5px ;
			color: #222222 ;
			padding: 5px 10px 5px 10px ;
		}
		button.on {
			background-color: #333333 ;
			border: 1px solid #000000 ;
			color: #f0f0f0 ;
		}

		.message {
			font-size: 20px ;
		}
		.message--big {
			font-size: 40px ;
		}
		.message--bold {
			font-weight: bold ;
		}
		.message--beautiful {
			color: hotpink ;
		}
		.message--scream {
			text-transform: uppercase ;
		}
	</style>
</head>
<body>

	<div x-data="{
		isBig: false,
		isBold: false,
		isBeautiful: false,
		isScream: false
		}">
		<p>
			<button @click="( isBig = ! isBig )" :class="{ on: isBig }">
				Biggify
			</button>
			<button @click="( isBold = ! isBold )" :class="{ on: isBold }">
				Boldify
			</button>
			<button @click="( isBeautiful = ! isBeautiful )" :class="{ on: isBeautiful }">
				Beautify
			</button>
			<button @click="( isScream = ! isScream )" :class="{ on: isScream }">
				Screamify
			</button>
		</p>
		<div
			class="message"
			x-cx:message--big="isBig"
			x-cx:message--bold="isBold"
			x-cx:message--beautiful="isBeautiful"
			x-cx:message--scream="isScream">
			Alpine.js Is Noice!
		</div>
	</div>

	<script type="text/javascript" src="../vendor/alpine.3.13.5.min.js" defer></script>
	<script type="text/javascript">

		document.addEventListener(
			"alpine:init",
			function setupAlpineBindings() {

				Alpine.directive( "cx", ClassNamesDirective );

			}
		);

		function ClassNamesDirective( element, metadata, framework ) {

			// The value is the token that comes after the ":" in the markup. For example,
			// in the attribute "x-cx:foo", the value is "foo". This holds the class name
			// that we want add / remove in the DOM.
			var className = metadata.value;

			// In order to respond to changes in the state over time, we have to create a
			// reactive expression that will be called when its dependencies change. In
			// order to avoid re-compiling the expression over and over, let's compile it
			// once as a getter that we can call later.
			var getState = framework.evaluateLater( metadata.expression );

			// Then, we can define a callback to be invoked immediately; and then
			// subsequently whenever the attribute expression changes.
			framework.effect( handleEffect );

			// ---
			// PRIVATE METHODS.
			// ---

			/**
			* I get called once when the expression is compiled and then subsequently
			* whenever the expression changes.
			*/
			function handleEffect() {

				getState( applyState );

			}

			/**
			* I apply the evaluated expression state to the current element.
			*/
			function applyState( isTruthy ) {

				if ( isTruthy ) {

					element.classList.add( className );

				} else {

					element.classList.remove( className );

				}

				// Log the current classname list for blog demo.
				console.log(
					"%cClass list:", "color: red ; font-weight: bold",
					element.classList.toString().replace( / /g, "\n + " )
				);

			}

		}

	</script>

</body>
</html>

Notice that I'm using the x-cx directive 4 times on the same element. And, that these 4 instances are working alongside the native class attribute:

<div
	class="message"
	x-cx:message--big="isBig"
	x-cx:message--bold="isBold"
	x-cx:message--beautiful="isBeautiful"
	x-cx:message--scream="isScream">
	....
</div>

As you can see, each x-cx attribute applies a different CSS class name and is driven by a different scope property (isBig, isBold, etc).

Internally, my ClassNamesDirective directive creates a "getter" out of the attribute expression. Then, it uses the .effect() callback to modify the DOM element whenever the expression is initialized or updated. Under the hood, I'm using the native .classList Element property to add or remove the annotated CSS class as needed.

When we run this Alpine.js demo and click on the buttons, we get the following output:

User clicking buttons to dyanmically add / remove CSS class names to a given element.

Now, keep in mind that Alpine.js already allows for dynamic CSS class name bindings via the inline object syntax. Which means, I could have accomplished the same functionality using this code:

<div
	class="message"
	:class="{
		'message--big': isBig,
		'message--bold': isBold,
		'message--beautiful': isBeautiful,
		'message--scream': isScream
	}">
	....
</div>

The difference is a matter of personal preference. In Angular, I use both of these techniques. Sometimes, using an object feels "right"; and, sometimes, using the per-attribute binding feels "right". It's simply nice to have the choice. And, it's great that I can polyfill that functionality in Alpine.js using a custom directive.

Want to use code from this post? Check out the license.

Reader Comments

15,848 Comments

In retrospect, I have no idea why I didn't just call this directive x-class 🤪 I think I was too deep in the weeds trying to figure out how it works, and forgot to pick an obvious name.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel