~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted Attributes work.
Attributes
- Additional configuration for HTML elements
- Control element behavior and provide metadata
- Bridge between HTML structure and browser/JavaScript behavior
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:
- Boolean attributes - Present = true, absent = false (disabled, checked, required)
- Enumerated attributes - Limited set of values (type, method, autocomplete)
- Global attributes - Work on any element (id, class, style, data-*)
- Event handler attributes - Inline event handlers (onclick, onchange) [discouraged]
- ARIA attributes - Accessibility information (aria-label, aria-hidden, role)
- 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:
- Property-vs-attribute is real and matters most for
value/checked/disabled. All four templates make sure those go on as properties, not attributes — otherwise the input won’t track user input correctly. React handles it implicitly, Angular’s[value]is property binding, Vue’s:valuedefers to its known cases, Lit asks you to be explicit with the.prefix. - Boolean attributes diverge from boolean properties.
<input disabled>(attribute present = true) vsel.disabled = true(property). Settingdisabled="false"as an attribute is the classic footgun — the attribute is present, so the input is disabled regardless of value. - Event handlers are sometimes attributes, often properties, never inline. Inline
onclick="..."was deprecated long ago. Each framework wires events through its native channel (synthetic event for React,(event)binding for Angular,@eventfor Vue,@event=${}for Lit) — but they all resolve toaddEventListenerunder the hood.
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>