Many UI designers can write good HTML and CSS but stop when something needs to react: a menu that opens, a tab that switches, a dark mode toggle. The good news is that most interface behaviour needs only a small part of JavaScript – the part that talks to the DOM. This guide covers exactly that part, with working examples you can edit.
You do not need to become a JavaScript engineer to benefit. Knowing these JavaScript DOM basics helps you prototype in the browser, understand what you are asking developers to build, and design interactions that are realistic.
What is the DOM?
When the browser reads your HTML, it builds a tree of objects in memory called the Document Object Model. Every element becomes a node in that tree. JavaScript can find any node, read it, change its text or attributes, add or remove classes, and listen for events such as clicks. When the DOM changes, the browser updates the screen.
Think of HTML as the blueprint and the DOM as the actual building. JavaScript is the person walking around the building, opening doors and switching lights on.
JavaScript DOM basics: the five things you will do most
| Task | Code | Example use |
|---|---|---|
| Find one element | document.querySelector('.menu') | Get the menu to open |
| Find many elements | document.querySelectorAll('.tab') | Loop over all tabs |
| Change classes | el.classList.toggle('is-open') | Show or hide with CSS |
| Change text or attributes | el.textContent = 'Saved', el.setAttribute('aria-expanded', 'true') | Update labels and states |
| React to the user | el.addEventListener('click', fn) | Run code on click, input, keydown |
querySelector uses the same selectors you already know from CSS – classes, IDs, attributes. That is why designers who know CSS learn DOM scripting quickly. The full details are in the DOM Manipulation and Events lessons.
The golden rule: JavaScript changes state, CSS changes looks
Beginners often write JavaScript like menu.style.display = 'block' and menu.style.color = 'red'. It works, but it mixes design into script, and every visual tweak needs a code change. A cleaner approach: JavaScript only adds or removes a class (or an attribute), and CSS decides what that state looks like.
.menu { display: none; }
.menu.is-open { display: block; }Now designers can change the open style – add an animation, change the colour – without touching the JavaScript at all.
Example 1: an accessible accordion
This FAQ accordion uses real <button> elements, so it works with the keyboard, and it updates aria-expanded so screen reader users know whether each panel is open.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accordion demo</title>
<style>
body { font-family: system-ui, sans-serif; margin: 16px; max-width: 520px; }
.acc-btn { width: 100%; text-align: left; font: inherit; font-weight: 600;
padding: 12px 16px; background: #f1f3f9; border: 1px solid #ccd2e0;
border-radius: 6px; cursor: pointer; margin-top: 8px; }
.acc-btn::after { content: "+"; float: right; }
.acc-btn[aria-expanded="true"]::after { content: "\2212"; }
.acc-panel { display: none; padding: 8px 16px; }
.acc-panel.is-open { display: block; }
</style>
</head>
<body>
<h1>Delivery FAQs</h1>
<button class="acc-btn" aria-expanded="false" aria-controls="p1">Do you deliver to Puducherry?</button>
<div class="acc-panel" id="p1"><p>Yes, within two working days.</p></div>
<button class="acc-btn" aria-expanded="false" aria-controls="p2">Can I pay cash on delivery?</button>
<div class="acc-panel" id="p2"><p>Yes, for orders under a set limit.</p></div>
<script>
document.querySelectorAll('.acc-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var panel = document.getElementById(btn.getAttribute('aria-controls'));
var open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
panel.classList.toggle('is-open', !open);
});
});
</script>
</body>
</html>Notice how the plus/minus icon is styled purely in CSS using the aria-expanded attribute selector. The state lives in one place, and both the visuals and the accessibility information stay in sync.
Tip: For a simple accordion, HTML also has the native <details> and <summary> elements, which need no JavaScript at all. Use them when they fit your design – fewer scripts means fewer bugs.
Example 2: tabs that switch content
Tabs are a common design pattern. This version shows the idea with a simple loop: remove the active state from all tabs, then add it to the clicked one.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tabs demo</title>
<style>
body { font-family: system-ui, sans-serif; margin: 16px; }
[role="tablist"] { display: flex; gap: 4px; border-bottom: 2px solid #ddd; }
[role="tab"] { font: inherit; padding: 8px 14px; border: 0; background: none; cursor: pointer;
border-bottom: 3px solid transparent; margin-bottom: -2px; }
[role="tab"][aria-selected="true"] { border-color: #2a5bd7; color: #2a5bd7; font-weight: 600; }
[role="tabpanel"] { padding: 12px 0; }
</style>
</head>
<body>
<div role="tablist" aria-label="Plan details">
<button role="tab" aria-selected="true" aria-controls="t1" id="tab1">Overview</button>
<button role="tab" aria-selected="false" aria-controls="t2" id="tab2" tabindex="-1">Features</button>
<button role="tab" aria-selected="false" aria-controls="t3" id="tab3" tabindex="-1">Reviews</button>
</div>
<div role="tabpanel" id="t1" aria-labelledby="tab1">A simple plan for small shops in Tirunelveli.</div>
<div role="tabpanel" id="t2" aria-labelledby="tab2" hidden>Billing, stock alerts and GST-ready invoices.</div>
<div role="tabpanel" id="t3" aria-labelledby="tab3" hidden>Rated well by early users.</div>
<script>
var tabs = document.querySelectorAll('[role="tab"]');
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
tabs.forEach(function (t) {
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
document.getElementById(t.getAttribute('aria-controls')).hidden = true;
});
tab.setAttribute('aria-selected', 'true');
tab.removeAttribute('tabindex');
document.getElementById(tab.getAttribute('aria-controls')).hidden = false;
});
});
</script>
</body>
</html>A production tabs component should also support the arrow keys to move between tabs, as described in the W3C ARIA Authoring Practices Guide. It is a good exercise: listen for keydown on the tab list and move focus with .focus().
Example 3: live form feedback
Designers often specify helpful inline messages, like a character counter. The input event fires every time the value changes:
const bio = document.querySelector('#bio');
const counter = document.querySelector('#bio-count');
const max = 160;
bio.addEventListener('input', () => {
const left = max - bio.value.length;
counter.textContent = left + ' characters left';
counter.classList.toggle('is-warning', left < 20);
});The events designers use most
An event is the browser telling your code that something happened. You do not need to learn all of them. These cover almost every interaction you will design:
| Event | Fires when | Typical design use |
|---|---|---|
click | A button or link is activated by mouse, touch, Enter or Space | Open menus, toggle panels, add to cart |
input | The value of a field changes | Live counters, instant search hints, password strength |
change | A select, checkbox or radio is changed and committed | Filters, settings switches |
submit | A form is submitted | Show a loading state, validate before sending |
keydown | A key is pressed | Close a popup with Escape, arrow-key navigation |
focus / blur | An element gains or loses focus | Show helper text next to a field |
Notice that click on a real button also fires for keyboard users. That is one more reason to use <button>: you write one handler and it works for everyone.
Storing small bits of data in HTML
Custom data-* attributes let you attach information to elements without inventing new classes. For example, a set of filter buttons can carry the category they filter by:
// <button class="filter" data-city="madurai">Madurai</button>
document.querySelectorAll('.filter').forEach((btn) => {
btn.addEventListener('click', () => {
const city = btn.dataset.city; // "madurai"
document.querySelectorAll('.hotel').forEach((card) => {
card.hidden = card.dataset.city !== city;
});
});
});The attribute data-city becomes dataset.city in JavaScript. Hyphenated names become camelCase, so data-room-type is read as dataset.roomType.
Respecting user preferences in script
CSS media queries such as prefers-reduced-motion can also be checked from JavaScript with window.matchMedia('(prefers-reduced-motion: reduce)').matches. Use it to skip auto-playing carousels or smooth scrolling for people who have asked their device for less motion. Designing for these settings is part of accessible interaction design, not an optional extra.
Where to put your script
Place your <script> just before </body>, or put it in the head with the defer attribute: <script src="main.js" defer></script>. Both make sure the HTML exists before your code looks for it. If querySelector returns null, the most common reason is that the script ran before the element was parsed.
Common mistakes when learning JavaScript DOM basics
- Using a
<div>with a click handler instead of a<button>. Divs are not focusable and do not respond to Enter or Space. - Using
innerHTMLwith user input. It can run injected code. UsetextContentfor plain text. - Forgetting that
querySelectorAllreturns a list – you must loop over it to add listeners. - Hiding content only visually while it stays reachable by keyboard. Use the
hiddenattribute ordisplay: nonefor content that is truly closed. - Animating with JavaScript what CSS can do. Toggle a class and let a CSS transition handle the motion – see CSS Animation Performance Tips.
Beyond the JavaScript DOM basics: where to go next
Once you are comfortable with these patterns, try building a dark mode toggle, a modal dialog using the native <dialog> element, or a filterable product list. Then read the JavaScript track from the start to fill in the language fundamentals. If your goal is a development career, see the Front-End Developer Roadmap.
Frequently asked questions
Do UI/UX designers need to learn JavaScript?
It is not required, but basic DOM skills help you prototype realistic interactions, understand technical limits and communicate better with developers.
Should I learn jQuery for DOM work?
Not for new projects. Modern browsers support querySelector, classList and addEventListener natively, which cover what most people used jQuery for.
What is the difference between textContent and innerHTML?
textContent sets plain text and treats any tags as text, which is safe. innerHTML parses a string as HTML, which is powerful but risky with user input.
Can I prototype interactions in the browser instead of Figma?
Yes, and for some interactions it is the better choice. Figma prototypes are fast for flows between screens, but things like live form validation, filtering a list or real keyboard behaviour are easier to judge in a small HTML, CSS and JavaScript prototype. Many teams use both: Figma for layout and flow, a browser prototype for the tricky interaction.
Why use aria-expanded instead of just a class?
A class only changes the visuals. aria-expanded tells assistive technology whether the section is open, so screen reader users get the same information as sighted users. You can style the attribute directly in CSS too.


