How to Build Dynamic UIs with HTML Data Attributes & JavaScript

Want to change content dynamically without messy ID selectors or hardcoded event listeners for every single button?

In this recipe, you will learn how to leverage HTML Data Attributes (data-*) alongside modern CSS and clean JavaScript to build a sleek, interactive content switcher component.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Data Attribute Tutorial</title>
    <!-- FontAwesome CDN for rendering clean modern UI icons -->
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
    />
    <!-- Link to external stylesheet -->
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <!-- Main container card structuring the tutorial component layout -->
    <div class="tutorial-card">
      <h2><i class="fa-solid fa-code"></i> Data Attribute Demo</h2>
      
      <!-- Button group container utilizing HTML data attributes (data-link) for dynamic routing/filtering -->
      <div class="buttons">
        <button class="btn active" data-link="home">
          <i class="fa-solid fa-house"></i> Home
        </button>
        <button class="btn" data-link="about">
          <i class="fa-solid fa-user"></i> About
        </button>
        <button class="btn" data-link="contact">
          <i class="fa-solid fa-envelope"></i> Contact
        </button>
      </div>

      <!-- Content wrapper where text updates dynamically based on button clicks -->
      <div class="content-display">
        <p class="text">Welcome to the Home Page!</p>
      </div>
    </div>

    <!-- Link to external JavaScript logic file -->
    <script src="script.js"></script>
  </body>
</html>

/* Import Google Font 'Poppins' for clean typography */
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap");

/* CSS Custom Properties (Design Tokens) for centralized theme styling */
:root {
  --primary-color: #4f46e5;
  --primary-hover: #4338ca;
  --bg-color: #f3f4f6;
  --card-bg: #ffffff;
  --text-main: #1f2937;
  --text-muted: #6b7280;
  --border-color: #e5e7eb;
  --radius: 12px;
  --transition: all 0.3s ease;
}

/* Global Reset to eliminate browser default margins and paddings */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Center the layout vertically and horizontally on the viewport */
body {
  font-family: "Poppins", sans-serif;
  background-color: var(--bg-color);
  color: var(--text-main);
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
}

/* Card container styling */
.tutorial-card {
  background-color: var(--card-bg);
  padding: 2.5rem;
  border-radius: var(--radius);
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
  width: 100%;
  max-width: 480px;
  text-align: center;
}

/* Card title typography and layout */
.tutorial-card h2 {
  font-size: 1.4rem;
  font-weight: 600;
  margin-bottom: 1.5rem;
  color: var(--text-main);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

.tutorial-card h2 i {
  color: var(--primary-color);
}

/* Flexbox container for action buttons */
.buttons {
  display: flex;
  justify-content: center;
  gap: 0.75rem;
  margin-bottom: 2rem;
}

/* Base button appearance and transition states */
.btn {
  background-color: transparent;
  border: 1px solid var(--border-color);
  padding: 0.75rem 1.25rem;
  font-size: 0.95rem;
  font-weight: 500;
  font-family: inherit;
  color: var(--text-muted);
  border-radius: 8px;
  cursor: pointer;
  transition: var(--transition);
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Hover feedback effect for buttons */
.btn:hover {
  border-color: var(--primary-color);
  color: var(--primary-color);
  background-color: rgba(79, 70, 229, 0.04);
}

/* Active class state highlighting the currently selected option */
.btn.active {
  background-color: var(--primary-color);
  color: #ffffff;
  border-color: var(--primary-color);
  box-shadow: 0 4px 12px rgba(79, 70, 229, 0.2);
}

/* Content display box area */
.content-display {
  background-color: var(--bg-color);
  padding: 2rem;
  border-radius: 8px;
  border: 1px dashed var(--border-color);
}

/* Text style with opacity transition configuration */
.text {
  font-size: 1.05rem;
  font-weight: 500;
  color: var(--text-main);
  transition: opacity 0.2s ease-in-out;
}

// Select all button elements and the target text container element from the DOM
const btns = document.querySelectorAll(".btn");
const text = document.querySelector(".text");

// Content dictionary mapping data attribute keys to respective display messages
const contentMap = {
  home: "Welcome to the Home Page!",
  about: "This is the About Us page where you can learn more.",
  contact: "Get in touch with us through the Contact page."
};

// Loop through each button node to attach an event listener for click actions
btns.forEach((btn) => {
  btn.addEventListener("click", (e) => {
    // Extract the value from the 'data-link' attribute using the dataset property (e.g., 'home', 'about', 'contact')
    const filter = e.currentTarget.dataset.link;

    // 1. Handle Active State Toggle: Remove 'active' class from all buttons, then add it to the clicked button
    btns.forEach((b) => b.classList.remove("active"));
    e.currentTarget.classList.add("active");

    // 2. Update Content Dynamically: Apply a smooth fade-out/fade-in effect when text content changes
    if (contentMap[filter]) {
      text.style.opacity = 0; // Fade out current text
      setTimeout(() => {
        text.textContent = contentMap[filter]; // Swap text value
        text.style.opacity = 1; // Fade in new text
      }, 150);
    }
  });
});

How the Code Works

  1. HTML Structure (index.html)Instead of writing separate event handlers for individual buttons, we embed custom data directly into the markup using the data-link attribute (data-link="home", data-link="about", data-link="contact"). This keeps the HTML semantic and extensible.
  2. Styling & Transitions (styles.css)The CSS utilizes CSS Custom Properties (design tokens) for a consistent theme, Flexbox for alignment, and a smooth opacity transition class (.text) to ensure text updates feel polished rather than jarring. An .active state clearly highlights the currently selected button.
  3. JavaScript Logic (script.js)
    • Event Delegation & Node Selection: The script loops through all .btn elements using querySelectorAll and attaches a click listener to each.
    • Dataset Access: When clicked, it extracts the target identifier using e.currentTarget.dataset.link.
    • State Management: It clears the .active class from all buttons and applies it solely to the clicked element.
    • Content Mapping: A structured lookup object (contentMap) pairs each data attribute key with its respective text string, updating the content dynamically with a clean fade effect.

Customization Notes: Variables You Can Modify

You can easily adapt this template to fit your specific project needs by modifying these key areas:

  • The Data Attribute Keys (index.html):You can change or add new buttons by altering the data-link="..." values (e.g., changing home to dashboard, or adding data-link="services").
  • The Content Dictionary (script.js):To add more options or change text, update the contentMap object keys to match your HTML data attributes, and modify the string values:
const contentMap = {
  home: "Welcome to your new custom home text!",
  about: "Update your about description here.",
  contact: "Modify your contact information here.",
  services: "Add a new key-value pair for custom pages!" // Example extension
};
  • Theme Colors (styles.css):Quickly change the primary brand color by editing the --primary-color variable inside the :root block:
:root {
  --primary-color: #4f46e5; /* Change this hex code to match your brand */
}

Leave a Comment