Skip to the content.
Agent Skill Available Download this Agent Skill (SKILL.md) to drop into ~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted Attributes work.
Download
attributes

Attributes

Key Insight

Attributes are the “settings panel” for HTML elements—they don’t change what an element is (a button is still a button), but they configure how it behaves, looks, and interacts with JavaScript. Understanding the difference between HTML attributes (in markup) and DOM properties (in JavaScript) is crucial: <input value="hello"> sets the attribute (initial value), but when users type, they’re changing the property (current value). This distinction breaks countless beginners who wonder why getAttribute('value') doesn’t return the current input text. Mastering attributes, data attributes, ARIA attributes, and the attribute/property relationship is essential for accessibility, data binding, and framework integration.

Detailed Description

Elements in HTML have attributes; these are additional values that configure the elements or adjust their behavior to meet the developer’s needs. Attributes appear in the opening tag and consist of a name and (usually) a value.

Attributes serve multiple purposes: some control element behavior (disabled, required), others provide metadata (id, class), some enable accessibility (aria-label, role), and others store custom data (data-* attributes). The browser parses attributes into DOM properties, but attributes and properties are not always synchronized—this is a common source of bugs.

Key categories of attributes:

  1. Boolean attributes - Present = true, absent = false (disabled, checked, required)
  2. Enumerated attributes - Limited set of values (type, method, autocomplete)
  3. Global attributes - Work on any element (id, class, style, data-*)
  4. Event handler attributes - Inline event handlers (onclick, onchange) [discouraged]
  5. ARIA attributes - Accessibility information (aria-label, aria-hidden, role)
  6. Data attributes - Custom data storage (data-user-id, data-config)

Code Examples

Basic Example: Setting attributes across frameworks

The same Input atom across the four UI templates: take props (id, name, type, value, disabled, placeholder, checked) and lay them onto a real <input> element. Each framework has its own answer to “is this an HTML attribute or a DOM property?” — the answers diverge most visibly for value and checked, which need property binding to track user input.

React

// templates/chota-react-redux/src/ui/atoms/Input/Input.component.jsx
// React decides at runtime whether to set an attribute or a property
// for each prop on the JSX element. `value` and `checked` are properties;
// `placeholder`, `type`, `id`, `name` are attributes. Spread does both.
export default function Input(props) {
  let id = props.id;
  if (!id) id = Math.random();
  return (
    <>
      <label htmlFor={id} className="sr-only">
        {props.name || "Some Label"}
      </label>
      <input {...props} id={id} />
    </>
  );
}

Angular

// templates/chota-angular-ngrx/src/ui/atoms/Input/Input.component.ts
// Angular distinguishes attribute binding ([attr.x]="...") from property
// binding ([x]="..."). Property is the default; the @HostBinding here
// reflects the inner type onto the host element only when no [type] is
// explicitly bound from the parent.
import { Component, Input, Output, EventEmitter, HostBinding } from '@angular/core';

@Component({
  selector: 'app-input',
  standalone: true,
  templateUrl: './Input.component.html',
  styleUrls: ['./Input.style.css'],
})
export default class InputComponent {
  @HostBinding('attr.type') externalType: string | null = '';

  @Input() set type(value: string) { this._type = value; this.externalType = null; }
  get type() { return this._type; }
  private _type = '';

  @Input() id = '';
  @Input() name = '';
  @Input() value = '';
  @Input() checked = false;
  @Input() disabled = false;
  @Input() placeholder = '';

  @Output() onInput = new EventEmitter<Event>();
  onInputHandler(e: Event) { this.onInput.emit(e); }
}
<!-- Input.component.html -->
<label [for]="id" class="sr-only">{{ name || 'Some Label' }}</label>
<input
  [id]="id"
  [type]="type"
  [value]="value"
  [checked]="checked"
  [disabled]="disabled"
  [placeholder]="placeholder"
  (input)="onInputHandler($event)"
/>

Vue

<!-- templates/chota-vue-pinia/src/ui/atoms/Input/Input.component.vue -->
<!-- :prop is shorthand for v-bind:prop. Vue chooses attribute vs property
     automatically (DOM attributes by default, switching to property for
     known cases like value/checked/innerHTML). -->
<template>
  <input
    :id="id"
    :name="name"
    :type="type || 'text'"
    :disabled="disabled"
    :alt="alt"
    :placeholder="placeholder"
    :class="getImageClass()"
    @click="$emit('onClick', $event)"
    @input="$emit('onInput', $event)"
    :value="value"
    :checked="checked"
  />
</template>

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  props: ['id', 'name', 'type', 'value', 'disabled', 'placeholder', 'alt', 'class', 'onInput', 'onClick', 'checked'],
  methods: {
    getImageClass() { return `${this.class}`; },
  },
});
</script>

Web Components

// templates/chota-wc-saga/src/ui/atoms/Input/Input.component.js
// Lit makes the attribute-vs-property distinction explicit:
//   foo=${...}       sets an attribute (everything stringifies)
//   .foo=${...}      sets a JS property (preserves type, required for
//                     value/checked because they diverge from attributes
//                     once the user types)
//   ?foo=${...}      sets a boolean attribute
//   @foo=${...}      attaches an event listener
import { html } from "lit";
import useComputedStyles from "../../../utils/theme/hooks/useComputedStyles";
import style from './Input.style';
import emit from "../../../utils/events/emit";

export default function Input(props) {
  useComputedStyles(this, [style]);
  let id = props.id;
  if (!id) id = Math.random();
  return html`
    <label htmlFor=${id} class="sr-only">
      ${props.name || "Some Label"}
    </label>
    <input
      type=${props.type}
      .value=${props.value}
      .checked=${props.checked}
      id=${props.id}
      name=${props.name}
      placeholder=${props.placeholder}
      @change=${(e) => emit(this, "onChange", e)}
      @input=${(e) => emit(this, "onInput", e)}
    />
  `;
}

Three things the tabs make obvious:

Practical Example: Data Attributes and Custom Metadata

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Data Attributes Example</title>
</head>
<body>
  <!-- Data attributes for custom data storage -->
  <div id="product-card"
       data-product-id="12345"
       data-product-name="Laptop"
       data-price="999.99"
       data-in-stock="true"
       data-category="electronics">
    <h3>Laptop</h3>
    <p class="price">$999.99</p>
    <button class="add-to-cart">Add to Cart</button>
  </div>
  
  <ul id="user-list">
    <li data-user-id="1" data-role="admin" data-active="true">
      Alice
      <button class="edit-user">Edit</button>
    </li>
    <li data-user-id="2" data-role="user" data-active="false">
      Bob
      <button class="edit-user">Edit</button>
    </li>
  </ul>
  
  <script>
    // Accessing data attributes
    const productCard = document.getElementById('product-card');
    
    // Method 1: dataset API (recommended)
    console.log(productCard.dataset.productId);    // "12345"
    console.log(productCard.dataset.productName);  // "Laptop"
    console.log(productCard.dataset.price);        // "999.99"
    console.log(productCard.dataset.inStock);      // "true" (string!)
    
    // Setting data attributes
    productCard.dataset.onSale = 'true';
    productCard.dataset.discountPercent = '10';
    // Creates: data-on-sale="true" data-discount-percent="10"
    
    // Method 2: getAttribute (works too)
    console.log(productCard.getAttribute('data-product-id'));  // "12345"
    
    // Event delegation with data attributes
    const userList = document.getElementById('user-list');
    
    userList.addEventListener('click', (e) => {
      if (e.target.matches('.edit-user')) {
        const listItem = e.target.closest('li');
        const userId = listItem.dataset.userId;
        const role = listItem.dataset.role;
        const active = listItem.dataset.active === 'true';  // Parse string
        
        console.log(`Editing user ${userId} (${role}, active: ${active})`);
      }
    });
    
    // Add to cart with data attributes
    document.querySelector('.add-to-cart').addEventListener('click', () => {
      const product = {
        id: productCard.dataset.productId,
        name: productCard.dataset.productName,
        price: parseFloat(productCard.dataset.price),
        inStock: productCard.dataset.inStock === 'true'
      };
      
      console.log('Adding to cart:', product);
    });
  </script>
</body>
</html>

Advanced Example: ARIA Attributes and Accessibility

<!-- ARIA Attributes for Accessibility -->
<nav role="navigation" aria-label="Main navigation">
  <button
    aria-expanded="false"
    aria-controls="mobile-menu"
    aria-label="Toggle navigation menu"
    id="menu-toggle">
    <span aria-hidden="true"></span>
  </button>
  
  <ul id="mobile-menu" aria-labelledby="menu-toggle" hidden>
    <li><a href="/home">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

<!-- Form with ARIA live regions -->
<form>
  <label for="username">
    Username
    <span aria-live="polite" aria-atomic="true" id="username-error"></span>
  </label>
  <input
    type="text"
    id="username"
    aria-required="true"
    aria-invalid="false"
    aria-describedby="username-help">
  <p id="username-help" class="help-text">
    Must be 3-20 characters
  </p>
  
  <button type="submit" aria-busy="false">Submit</button>
</form>

<!-- Tab panel with ARIA -->
<div role="tablist" aria-label="Content tabs">
  <button
    role="tab"
    aria-selected="true"
    aria-controls="panel-1"
    id="tab-1">
    Tab 1
  </button>
  <button
    role="tab"
    aria-selected="false"
    aria-controls="panel-2"
    id="tab-2">
    Tab 2
  </button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  Content for tab 1
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  Content for tab 2
</div>

<script>
  // Managing ARIA attributes dynamically
  const menuToggle = document.getElementById('menu-toggle');
  const mobileMenu = document.getElementById('mobile-menu');
  
  menuToggle.addEventListener('click', () => {
    const expanded = menuToggle.getAttribute('aria-expanded') === 'true';
    
    menuToggle.setAttribute('aria-expanded', !expanded);
    mobileMenu.hidden = expanded;
  });
  
  // Form validation with ARIA
  const usernameInput = document.getElementById('username');
  const usernameError = document.getElementById('username-error');
  
  usernameInput.addEventListener('blur', () => {
    const value = usernameInput.value;
    
    if (value.length < 3 || value.length > 20) {
      usernameInput.setAttribute('aria-invalid', 'true');
      usernameError.textContent = 'Username must be 3-20 characters';
    } else {
      usernameInput.setAttribute('aria-invalid', 'false');
      usernameError.textContent = '';
    }
  });
  
  // Tab switching with ARIA
  document.querySelectorAll('[role="tab"]').forEach(tab => {
    tab.addEventListener('click', () => {
      // Deselect all tabs
      document.querySelectorAll('[role="tab"]').forEach(t => {
        t.setAttribute('aria-selected', 'false');
      });
      
      // Select clicked tab
      tab.setAttribute('aria-selected', 'true');
      
      // Show corresponding panel
      document.querySelectorAll('[role="tabpanel"]').forEach(panel => {
        panel.hidden = true;
      });
      
      const panelId = tab.getAttribute('aria-controls');
      document.getElementById(panelId).hidden = false;
    });
  });
</script>

Common Mistakes

1. Confusing Attributes with Properties

Mistake: Expecting attribute changes to always sync with properties.

<input type="text" value="initial">

<script>
// ❌ Misunderstanding attribute/property relationship
const input = document.querySelector('input');

// User types "hello" in the input
console.log(input.value);  // "hello" (property - current value)
console.log(input.getAttribute('value'));  // "initial" (attribute - HTML value)

// Attribute doesn't update with user input!
input.value = 'new value';  // Changes property
console.log(input.getAttribute('value'));  // Still "initial"

// ✅ Correct understanding
console.log('Current value (property):', input.value);
console.log('Initial value (attribute):', input.getAttribute('value'));

// Set both if needed
input.value = 'new value';  // Update property (what user sees)
input.setAttribute('value', 'new value');  // Update attribute (HTML)
</script>

Why it matters: Attributes are initial values; properties are current values. They diverge for certain attributes like value, checked.

2. Treating Boolean Attributes as Strings

Mistake: Setting boolean attributes to “false” thinking it disables them.

<!-- ❌ BAD: All these are TRUE -->
<input required="false">  <!-- Attribute present = true! -->
<input disabled="no">  <!-- Still disabled! -->
<button disabled="0">Submit</button>  <!-- Still disabled! -->

<script>
// ❌ BAD: Setting string "false"
button.setAttribute('disabled', 'false');  // Still disabled!

// ✅ GOOD: Remove attribute to set false
button.removeAttribute('disabled');  // Now enabled

// Or use property (better)
button.disabled = false;  // Removes attribute
button.disabled = true;   // Adds attribute
</script>

Why it matters: Boolean attributes are true if present, regardless of value. Remove attribute or use property = false.

3. Not Hyphenating data Attribute Names

Mistake: Using camelCase in HTML instead of kebab-case.

<!-- ❌ BAD: Won't work as expected -->
<div data-userId="123"></div>

<script>
const div = document.querySelector('div');

// Doesn't work - attribute name is case-insensitive in HTML
console.log(div.dataset.userId);  // undefined!

// HTML normalized it to lowercase
console.log(div.getAttribute('data-userid'));  // "123"
console.log(div.dataset.userid);  // "123"
</script>

<!-- ✅ GOOD: Use kebab-case in HTML -->
<div data-user-id="123"></div>

<script>
// dataset converts to camelCase
console.log(div.dataset.userId);  // "123" ✓
</script>

Why it matters: HTML attributes are case-insensitive. Use kebab-case in HTML, access as camelCase via dataset.

Boolean Attributes

Boolean Attributes (e.g. required, readonly, disabled). If a boolean attribute is present, its value is true, and if it’s absent, its value is false. HTML defines restrictions on the allowed values of boolean attributes: If the attribute is present, its value must either be the empty string (equivalently, the attribute may have an unassigned value), or a value that is an ASCII case-insensitive match for the attribute’s canonical name, with no leading or trailing whitespace.

To be clear, the values "true" and "false" are not allowed on boolean attributes. To represent a false value, the attribute has to be omitted altogether. This restriction clears up some common misunderstandings: With checked="false" for example, the element’s checked attribute would be interpreted as true because the attribute is present.

The following examples are valid ways to mark up a boolean attribute:

<div itemscope>This is valid HTML but invalid XML.</div>
<div itemscope="itemscope">This is also valid HTML but invalid XML.</div>
<div itemscope="">This is valid HTML and also valid XML.</div>
<div itemscope="itemscope">
  This is also valid HTML and XML, but perhaps a bit verbose.
</div>

Event Handler Attributes

WARNING: The use of event handler content attributes is discouraged. The mix of HTML and JavaScript often produces unmaintainable code, and the execution of event handler attributes may also be blocked by content security policies.

All event handler attributes accept a string. The string will be used to synthesize a JavaScript function like function name(/*args*/) {body}, where name is the attribute’s name, and body is the attribute’s value. The handler receives the same parameters as its JavaScript event handler counterpart — most handlers receive only one event parameter, while onerror receives five: event, source, lineno, colno, error. This means you can, in general, use the event variable within the attribute.

<div onclick="console.log(event)">Click me!</div>
<!-- The synthesized handler has a name; you can reference itself -->
<div onclick="console.log(onclick)">Click me!</div>

Quick Quiz

What is the difference between an HTML attribute and a JS property on a DOM element?

How do boolean attributes in HTML work?

What are data-* attributes best used for?

Why do ARIA attributes matter even when the UI looks fine visually?