x-bind in Alpine.js: A complete guide to dynamic classes and styles

- Andrés Cruz - ES En español

Video thumbnail

When I started working with Alpine.js — installing it and taking your first steps, one of the first things that surprised me was that x-bind works almost the same way as in Vue, but within a much smaller, simpler, and faster framework to implement. That "Vue in miniature" feeling is precisely what makes Alpine perfect when you don't want to set up a full application just to add specific dynamism to your UI.

In this guide, I explain with clear examples how x-bind works, when to use it, its shortcuts, typical errors, and how it can help you build clean and reactive micro-interactions without installing anything beyond a CDN script.

What is x-bind and what is it for?

The x-bind directive allows you to bind HTML attributes to the reactive state defined in x-data.
Whenever that state changes, Alpine automatically updates the attribute in the DOM, without you having to manipulate the HTML manually.

Examples of attributes you can bind with x-bind:

  • disabled
  • readonly
  • value
  • id
  • class
  • style
  • any valid HTML attribute

Something I love about Alpine is that it lets you work with native DOM attributes without complex layers. In Vue, I felt more "structure"; here, on the other hand, everything is direct and very HTML-driven. The result is the same, but the weight is minimal.

Main Alpine.js Directives

If you come from other frameworks like Vue or React, learning Alpine will be very easy. Its core operations are summarized in a few directives that will surely already look familiar to you:

DirectiveDescription
x-dataDefines the reactive data block (the component).
x-bind (or :)Binds HTML attributes to the state (classes, styles, etc.).
x-on (or @)Listens for DOM events (like a click or an input).
x-text / x-htmlDisplays text or HTML content on screen reactively.
x-show / x-ifShows or hides elements based on a condition.
x-forIterates over a list of items and renders one for each item.
x-transitionAdds smooth animations to visibility changes.

✍️ Basic syntax: x-bind:attribute and its short version (:)

Full syntax

<button x-bind:disabled="isDisabled">Submit</button>

Shorthand syntax

<button :disabled="isDisabled">Submit</button>

Both ways are equivalent and produce exactly the same result. Alpine interprets the : prefix as an alias for x-bind:, the same way Vue does.

In my experience, the short : syntax makes you move faster, especially when coming from Vue: the learning curve is almost nonexistent because you already have the syntax internalized.

Below is an example that applies binding to the disabled attribute of two buttons to simulate a selection. Notice how both the long form (x-bind:disabled) and the short form (:disabled) are used:

<div x-data="{ woman:false }">
    <!-- Long form: x-bind:disabled -->
    <button x-bind:disabled="!woman" @click="woman=false">
        Man
    </button>
    <!-- Short form: :disabled -->
    <button :disabled="woman" @click="woman=true">
        Woman
    </button>
</div>

Here the woman variable is initialized to false. The "Man" button receives x-bind:disabled="!woman", meaning it will be disabled when woman is false (using the negation operator !). The "Woman" button uses the shorthand syntax :disabled="woman" and is disabled when woman is true.

Let's remember that, up to this point, we have worked with custom attributes, that is, Alpine's x-* directives. These attributes are not part of the HTML standard.

What do I mean? Attributes like x-data (and all x-* directives) are not part of the HTML standard. They only make sense if Alpine is imported into the page. If we removed the Alpine script, the x-* attributes would mean absolutely nothing to the browser: they would be ignored or treated as unknown attributes. They only acquire power when Alpine interprets them and acts accordingly.

When to use shorthand syntax

  • When you already master the binding idea and want more readable code.
  • When you are going to use many bound attributes and want more compact code.
  • When you come from Vue (like me) and find it natural to write :disabled or :value.
  • When you need to rapidly prototype a UI without wasting time on verbosity.

Standard HTML attributes in Alpine

Beyond Alpine's own directives (x-*), when we use x-bind to bind values to attributes, we are working with attributes that do belong to the HTML standard. A clear example is disabled: it is a native attribute of elements like <button>, <input>, or <select>, and Alpine simply enables or disables it reactively.

This is a quick review —I assume you already know this—, but it serves to get into context: if an element is enabled and we apply disabled, the browser disables it visually and functionally.

Depending on the type of HTML element, we have different attributes available. For example, in the <input> element we can also use attributes like disabled or readonly:

<button :disabled="woman" @click="woman=true">
    Woman
</button>

Let's take the woman variable as an example and initialize it to true by default. When selecting the corresponding button, the user indicates a preference, which can enable or disable parts of the interface. Later we will cover this in depth with conditionals.

Here is the complete structure of the component:

<div x-data="{ woman:false }">
    <!-- Equivalent: x-bind:disabled="woman==false" -->
    <button x-bind:disabled="!woman" @click="woman=false">
        Man
    </button>
    <!-- Equivalent: :disabled="woman==true" -->
    <button :disabled="woman" @click="woman=true">
        Woman
    </button>
</div>

Class binding with x-bind:class

Video thumbnail

Following the same line of reactivity, we are now going to look at CSS class binding, which is more interesting because the syntax changes slightly compared to a simple true/false. Here a JavaScript object comes into play that associates classes with conditions.

The directive we use is x-bind:class (or its shorthand form, :class).

1. Basic syntax (conditional)

In Alpine.js, to apply classes conditionally, a JavaScript object syntax is used inside the directive. The structure is as follows:

Syntax: { 'class-to-apply': booleanExpression }

Logic: the class is applied if and only if the boolean expression is true.

<div x-data="{ woman: true }">
   <button x-bind:class="{ 'hidden': !woman }" x-on:click="woman = false">Man</button>
   <button :class="{ 'hidden': woman }" @click="woman = true">Woman</button>
   <p x-text="woman"></p>
</div>

In the previous example:

  1. The "Man" button receives the hidden class when !woman is true, that is, when the woman variable is false.
  2. The "Woman" button receives the hidden class when the woman variable is true.

An important detail: Alpine automatically preserves static classes the element already has. If the element has class="btn btn-primary" and you use :class="{ 'hidden': isHidden }", Alpine only adds or removes hidden without touching btn btn-primary. This behavior avoids having to rewrite static classes inside the directive and is one of the reasons why x-bind:class is so convenient in practice.

Note: You can use either x-bind:class or :class interchangeably. Although some editors like Visual Studio Code might not recognize the shorthand syntax with full autocomplete, both forms are equivalent in Alpine.

2. Applying multiple conditional classes

If you want to apply several classes with independent conditions, you simply add more key: condition pairs separated by commas inside the same object. Each class evaluates its condition completely independently.

Syntax and multiple logic

The following example simulates a component that applies a border class (border) and a background class (bg) based on their respective boolean properties:

<style>
   .box { width: 100px; height: 100px; margin-top: 10px; }
   .border { border: 1px solid black; }
   .bg { background-color: #f0f8ff; }
</style>
<div x-data="{ border: false, bg: false }">
   <button @click="border = !border">Border</button>
   <button @click="bg = !bg">Background</button>

   <!-- Explicit form -->
   <div class="box" :class="{ 'border': border , 'bg' : bg }"></div>
   <!-- Shorthand form (object shorthand) -->
   <div class="box" :class="{ border, bg }"></div>
</div>

In this case, the class name ('border' or 'bg') is the key of the object, and the Alpine property (border or bg) is the value. If the property is true, the class is added to the element; if it is false, it is removed.

Look at the second version of the div: when the class name matches the property name in x-data exactly, you can use JavaScript's object shorthand ({ border, bg }), which is equivalent to { 'border': border, 'bg': bg }. It is a cleaner and more concise way that avoids repetition.

Important: the boolean expression for each class is completely independent. You can evaluate another property, a different variable, or the result of a complex function within the same directive without any problem.

When inspecting the element in the browser's DevTools, you will be able to see in real-time how the border or bg class is added to or removed from the DOM with each click, which evidences the stylistic reactivity that Alpine offers us.

Dynamic style binding with x-bind:style

Video thumbnail

Continuing with attribute work, we are now going to learn how to handle the style attribute dynamically in Alpine.js. The logic is similar to what we already saw with classes, but the way the value is constructed has its own particularities.

As always, the starting point is the x-data directive, where you define the component's variables. In this case, instead of booleans, we will work with strings representing CSS values like the border or background color.

For this exercise, we will define variables with initial CSS values in the x-data block, and based on button clicks we will change those variables, observing how Alpine reflects style changes in real-time thanks to its reactivity.

1. Definition and composition of the component

We define the root <div> with the initial data. In this case, border stores the string for the border CSS property and bg stores the background color:

<div x-data="{ border:'1px solid black', bg:'red' }">
   <button @click="border='1px solid black'">Border 1</button>
   <button @click="border='2px solid red'">Border 2</button>
   <p x-text="border"></p>

   <button @click="bg='red'">bg 1</button>
   <button @click="bg='blue'">bg 2</button>
   <p x-text="bg"></p>

   <div class="box" :style="'border: '+border+'; background:'+bg"></div>
</div>

Reactivity verification: initially, it is a good practice to use x-text in <p> tags to confirm that the values of border and bg change correctly before connecting them with style binding. Thus, if something doesn't work, you can rule out issues in the reactive state and focus on the :style syntax.

2. Implementing style binding

For Alpine to evaluate what we pass as a style and apply it dynamically, we use the abbreviation :style (equivalent to x-bind:style).

Crucial here is respecting the structure of the HTML inline style attribute: each property is defined as property: value;, and if there is more than one, they are separated with a semicolon (;). Since the value of :style is a JavaScript expression, we need to construct that string dynamically with the x-data variables.

Option A: Classic concatenation (using +)

This option uses the + operator to join fixed text strings with dynamic variables. It is important to add the ; separator between CSS properties and respect spaces:

<div class="box" :style="'border: '+border+'; background:'+bg"></div>

Option B: Template literals (recommended way)

For multiple styles or complex concatenations, the recommended option is JavaScript template literals. They are defined with backticks (`) and inject variables using the ${variable} syntax. The result is cleaner and less prone to typos than classic concatenation:

<div class="box" :style="`border: ${border}; background: ${bg};`"></div>

Additionally, Alpine also accepts a JavaScript object as the value of :style, following the same logic as class binding. In this case, keys are CSS properties in camelCase and values are reactive expressions:

<div class="box" :style="{ border: border, backgroundColor: bg }"></div>

This object syntax is especially useful when you want to bind individual CSS properties without needing to build a complete CSS string.

Moral of the story

The key when working with x-bind:style is the following:

Respect the HTML/CSS structure: You must replicate the inline styles structure: property: value;. If you use the string form, make sure the final result is valid CSS.

Use JS concatenation or template literals: Use the + operator or ${variable} syntax in template literals to dynamically inject values defined in x-data. If you prefer greater semantic clarity, use the object syntax with CSS properties in camelCase.

In any of the three cases, the binding works correctly, allowing the element to change style reactively with each interaction.

Quick comparison with Vue (from my experience)

Binding similarities

  • The concept is practically identical: attribute = expression.
  • The shorthand with : is the same as in Vue.
  • Reactivity is reflected immediately in the DOM without requiring additional configuration.

The first time I used Alpine I thought: "This is like Vue but without having to set up a whole project structure". Zero configuration, zero build tools, zero node_modules to manage.

Advantages when you don't want to set up a full app

This is where Alpine truly shines.

I use it when:

  • I only need two or three quick interactions on a page.
  • I am working on a static site or a backend with Blade/Twig.
  • I want reactivity without build tools or webpack or Vite configuration.
  • I want to keep HTML clean, straightforward, and easy to read.

Precisely for these things Alpine saves me time: it is HTML with a little reactive magic. No setups, no compiling, no installing anything.

⚠️ Common mistakes using x-bind and how to avoid them

  • ❌ Writing complex logic directly inside the attribute
    • ✔ Solution: move the logic to a computed property or a method inside x-data. Your template will look much cleaner and more maintainable.
  • ❌ Forgetting that native attributes have their own behavior
    • ✔ Remember that Alpine does not invent new attributes (except x-* ones). If the HTML attribute does not exist on the element, Alpine will add it to the DOM, but the browser might ignore it.
  • ❌ Using uninitialized properties in x-data
    • ✔ Always define and give an initial value to your properties inside x-data. If you don't, Alpine might evaluate the expression as undefined and behave unexpectedly.
  • ❌ Confusing x-bind with x-model
    • ✔ x-bind → binds HTML attributes unidirectionally (from state to DOM).
    • ✔ x-model → creates a two-way binding, designed for <input>, <select>, and <textarea>.

Practical cases: useful micro-interactions

Dynamic forms with :disabled and x-model

<div x-data="{ accepted:false }">
  <input type="checkbox" x-model="accepted"> I accept terms
  <button :disabled="!accepted">Continue</button>
</div>

Simple UI states without heavy frameworks

<div x-data="{ open:false }">
  <button @click="open = !open">Show menu</button>
  <nav x-show="open">Content</nav>
</div>

Dynamic classes based on active state (active tab, selected item)

<div x-data="{ active: 'home' }">
  <button :class="{ 'active': active === 'home' }" @click="active = 'home'">Home</button>
  <button :class="{ 'active': active === 'blog' }" @click="active = 'blog'">Blog</button>
</div>

Precisely for these things Alpine saves me time: it is HTML with a bit of reactive magic. No setups, no compiling, no installing anything.

❓ Frequently asked questions about x-bind

  • What is x-bind in Alpine.js?
    • It is a directive that binds HTML attributes to the reactive state defined in x-data. When the state changes, the attribute is automatically updated in the DOM.
  • How does it differ from Vue?
    • In syntax, almost not at all: the : shorthand and object logic for classes are identical. The difference lies in the ecosystem: Alpine does not require components, a build system, or additional configuration.
  • Is it mandatory to use the long version x-bind:attribute?
    • No. You can use the shorthand form :attribute freely, as Alpine interprets both in the same way.
  • Can I bind classes and styles at the same time?
    • Yes, both are supported and can coexist on the same element. Alpine manages them independently.
  • Are x-bind and x-model the same?
    • No. x-bind is unidirectional (from state to DOM). x-model creates a two-way binding and is designed for form elements like <input> or <select>.

Conclusion

x-bind is probably one of the most important and versatile directives in Alpine.js.
Its simplicity, coupled with its similarity to Vue, makes it a perfect tool when you are looking for lightweight reactivity without heavy frameworks. Whether you want to bind a simple disabled, conditionally apply CSS classes with :class, or build dynamic styles with :style, Alpine solves it with very few lines of code.

In my experience, it is the ideal bridge between classic HTML and a modern dynamic interface: without complications, without configuration, and without sacrificing markup readability.

Now, learn how to use loops and conditionals in Alpine to take reactivity one step further.

Learn how to use the Alpine.js `x-bind` directive to apply conditional classes, dynamic styles, and reactive HTML attributes. Includes both full and shorthand syntax, along with practical examples.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.