DEV Community

Connie Leung
Connie Leung

Posted on

3 1 3 1

Day 9 - Dynamic Binding of CSS Classes and Styles in Vue 3, Svelte 5 and Angular

Tables of Contents

On day 9, I will show how Vue 3, SvelteKit, and Angular perform dynamic CSS Class and Style binding. There is an example of CSS Class binding and Style binding. When an item is purchased, strikeout class is applied. When an item is considered high priority, priority class is used. When the high priority checkbox is checked in the Add Item Form, the text is bolded by the font-weight style.

Case 1: Dynamic CSS Class Binding to List Item

  • Vue 3 application
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref, computed } from 'vue'

type Item = { id: number; label: string; highPriority: boolean; purchased: boolean }

const items = ref<Item[]>([])

const togglePurchase = (item: Item) => {
  item.purchased = !item.purchased
}
</script>

<template>
    <ul>
      <div class="list-item" v-for="item in items" :key="item.id">
        <li
          :class="[{ priority: item.highPriority }, { strikeout: item.purchased }]" 
           @click="togglePurchase(item)"
        >
          {{ item.id }} - {{ item.label }}
        </li>
      </div>
    </ul>
</template>
Enter fullscreen mode Exit fullscreen mode

In Vue 3, an element's CSS class can be bound to an array or Object. In this example, I passed an array to the CSS class of the <li> elements.

{ strikeout: item.purchased } - when the purchased property of an item is true, the strikeout class is enabled. Otherwise, the class is disabled. When clicking the <li> element, the click event occurred, and the togglePurchase function toggled the property. As a result, the strikeout class was programmatically applied.

{ priority: item.highPriority } - similarly, the priority class is applied to the <li> element when the highPriority property of an item is true. When users check the High Priority checkbox and save the item, the property is true, and the text is displayed in a different text color.

If we want to pass Object to the class, the solution is :class="{ priority: item.highPriority, strikeout: item.purchased }"

  • SvelteKit application
<script lang="ts">
    import Icon from '@iconify/svelte';

    type Item = { id: number; label: string; purchased: boolean; higherPriority: boolean };

    let items = $state([] as Item[]);

    function togglePurchased(item: Item) {
        item.purchased = !item.purchased;
        newItem = '';
        newItemHigherPriority = false;
    }
</script>

<ul>
{#each items as item (item.id)} 
   <div class="list-item">
       <li class={[item.purchased && 'strikeout', item.higherPriority && 'priority']}>{item.id} - {item.label}</li>
       <button class="btn" onclick={() => togglePurchased(item)} aria-label="purchase an item">
         <Icon icon={!item.purchased ? "ic:baseline-check" : "ic:baseline-close" } />
     </button>
   </div>
{/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

The Svelte compiler issues a warning that <li> should not be clickable. Therefore, I add a button to invoke the togglePurchase function to toggle the purchased property.

Like Vue 3, an element’s CSS class can be bound to an array or Object. In this example, I passed an array to the CSS class of the <li> elements.

{ item.purchased && 'strikeout' } - when the purchased property of an item is true, the strikeout class is enabled. Otherwise, the class is disabled.

{ item.highPriority && 'priority' } - similarly, the priority class is applied to the <li> element when the highPriority property of an item is true. When users check the High Priority checkbox and save the item, the property is true, and the text is displayed in a different text color.

If we want to pass Object to the class, the solution is :class="{{ priority: item.highPriority, strikeout: item.purchased }}"

  • Angular 19 application
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';

type Item = { id: number; label: string; purchased: boolean; highPriority: boolean };

@Component({
  selector: 'app-shopping-cart',
  imports: [],
  template: `
     <ul>
        @for (item of items(); track item.id) {
          @let itemClasses =
            {
              priority: item.highPriority,
              strikeout: item.purchased,
            };
          <div class="list-item">
            <li [class]="itemClasses" (click)="togglePurchase(item)">
                {{ item.id }} - {{ item.label }}
            </li>
          </div>
        }
    </ul>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
  items = signal<Item[]>([]);
  newItem = signal('');
  newItemHighPriority = signal(false);

  togglePurchase(item: Item) {
    this.items.update((items) => 
      return items.map((element) => (element.id === item.id ? { ...element, purchased: !element.purchased } : element));
    });
    this.newItem.set('');
    this.newItemHighPriority.set(false);
  }
}
Enter fullscreen mode Exit fullscreen mode

Angular introduced the @let syntax, which can create a temporary variable in the HTML template.

@let itemClasses = {
    priority: item.highPriority,
    strikeout: item.purchased,
};
Enter fullscreen mode Exit fullscreen mode

itemClasses is { priority } when item.highPriority is true. itemClasses is { strikeout } when item.purchased is true. Multiple classes can be merged into an Object. When both properties are true, itemClasses is { priority, strikeout }.

Similar to attribute binding, box syntax is used for class binding. [class]="itemClasses" passes the itemClasses temporary variable to the CSS class of the <li> element.

Case 2: Dynamic Style Binding to High Priority Checkbox

  • Vue 3 application

When the newItemHighPriority ref is true, the font-weight style of the High Priority checkbox is set to bold. Otherwise, it is set to normal.
{ font-weight: newItemHighPriority ? 'bold': 'normal' } is passed to style attribute to apply the inline style.

<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref, computed } from 'vue'

const newItemHighPriority = ref(false)

const saveItem = () => {   same logic as before  }
</script>

<template>
  <form  v-if="isEditing" @submit.prevent="saveItem">
    <label>
      <input type="checkbox" v-model="newItemHighPriority" />
      <span :style="{ 'font-weight': newItemHighPriority ? 'bold' : 'normal' }">
        High Priority</span
      >
    </label>
  </form>
</template>
Enter fullscreen mode Exit fullscreen mode
  • SvelteKit application

When the newItemHighPriority rune is true, the font-weight style of the High Priority checkbox is set to bold. Otherwise, it is set to normal.
style:font-weight={ newItemHighPriority ? 'bold': 'normal' } passes the CSS value to the font-weight inline style.

<script lang="ts">
    import Icon from '@iconify/svelte';

    let newItemHigherPriority = $state(false);

    async function handleSubmit(event: SubmitEvent) {
    ... same logic as before ...
    }
</script>

<form method="POST" onsubmit={handleSubmit}>
   <label>
     <input id="higherPriority" name="higherPriority"                   type="checkbox" bind:checked={newItemHigherPriority}
      />
      <span style:font-weight={newItemHigherPriority ? 'bold' : 'normal'}> Higher Priority</span>
   </label>
</form>
Enter fullscreen mode Exit fullscreen mode
  • Angular 19 application

When the newItemHighPriority signal is true, the font-weight style of the High Priority checkbox is set to bold. Otherwise, it is set to normal.

Both style and class use the box syntax for binding. [style.fontWeight]="newItemHighPriority ? 'bold': 'normal'". The ternary expression returns bold when true and normal otherwise.

The CSS style uses the camel case in Angular. For example, "font-weight" replaces the dash character with a blank, and "weight" is capitalized. Therefore, font-weight is converted to fontWeight.

The style also accepts Object. The equivalent is [style]={ fontWeight: newItemHighPriority: 'bold' : 'normal' }.

import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-shopping-cart',
  imports: [FormsModule],
  template: `
      <form (ngSubmit)="saveItem()">
        <label>
          <input type="checkbox" [(ngModel)]="newItemHighPriority" name="newItemHighPriority" />
          <span [style.fontWeight]="newItemHighPriority() ? 'bold' : 'normal'"> High Priority</span>
        </label>
      </form>
  `,
   changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
  newItemHighPriority = signal(false);

  saveItem() {  same logic as before  }
}
Enter fullscreen mode Exit fullscreen mode

Resources

Github Repositories

Github Pages

We have successfully updated the shopping cart component to pass values to CSS classes or inline styles programmatically.

`

Top comments (3)

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

nice seeing all three side by side like this tbh - i remember wrestling with class binding the first time, so it's always a relief when frameworks make it simple. you ever find one method just feels smoother to maintain long-term, or is it kinda all personal preference?

Collapse
 
dotallio profile image
Dotallio

Super clear side by side breakdown, really makes it easy to pick up the subtle differences! Have you thought about including React or SolidJS in this comparison too?

Collapse
 
railsstudent profile image
Connie Leung

I heard SolidJS is a "Better React," so I am open to learning SolidJS.
My immediate goal is to get better at Vue 3 for work and Svelte where I heard is very performant.