Lesson · Beginner
DOM basics with JavaScript
Query existing semantic elements, respond to events, update text and state, and preserve focus, security, and a useful no-script baseline.
- Published
- Last checked
What this solves
The DOM lets a script enhance an already meaningful document. You can find an element, listen for a user action, update text or attributes, and create small stateful behaviors without replacing the page with a client-only shell.
Start from the no-script outcome. The script should improve feedback or efficiency while ordinary links, headings, disclosures, and form contracts keep their native behavior.
Minimal correct example
<button type="button" aria-controls="tip" aria-expanded="false">
Show review tip
</button>
<p id="tip" hidden>Check the page with a keyboard before publishing.</p>
<script type="module">
const button = document.querySelector('[aria-controls="tip"]');
const tip = document.querySelector('#tip');
if (
button instanceof HTMLButtonElement &&
tip instanceof HTMLParagraphElement
) {
button.addEventListener('click', () => {
const expanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!expanded));
tip.hidden = expanded;
});
}
</script>
The native button supplies focus and activation. Script synchronizes visible content with expanded state.
How browsers actually behave
The parser creates nodes and a tree. Deferred and module scripts run after the document has been parsed, while an early classic script can pause parsing and fail to find later elements. Query methods return no match when a selector is wrong, so guard the result rather than forcing an assumption.
Events travel through capture, target, and bubble phases. Event delegation can handle stable container behavior efficiently, but handlers still need to verify the target. DOM updates can trigger style, layout, paint, focus changes, and accessibility-tree updates.
Decisions and trade-offs
Use textContent for untrusted or ordinary text. innerHTML parses executable
markup and requires a deliberately sanitized source; it is not a convenient text
setter. Use data attributes for bounded private state hooks, but keep public
semantics in native elements and attributes.
Update the smallest stable node. Replacing an entire container can erase focus, text selection, form values, media state, and event listeners. A framework may manage these concerns at scale, but the underlying browser consequences remain.
Accessibility consequences
Visible and programmatic states must change together. If content opens,
aria-expanded, hidden state, focusability, and the visual indicator must
agree. Minor updates should not steal focus. Relevant asynchronous results may
need a restrained live status, while a new modal context needs deliberate focus
entry and return.
Respect reduced motion and preserve source order. A DOM insertion that looks correct at one breakpoint can appear in the wrong reading or focus position.
Common failures
- The selector returns null. The script ran too early or the contract changed. Defer initialization and guard expected element types.
- User text executes as markup. It was assigned through
innerHTML. Use textContent or a reviewed sanitizer for an approved markup feature. - A rerender loses focus. The focused subtree was replaced. Update the stable node or restore context intentionally.
- Visible and announced state disagree. CSS and ARIA were updated separately. Derive both from one bounded state transition.
- One handler fires for unrelated children. Delegation does not validate its target. Match the intended control before acting.
Verify it
Load with scripts blocked and confirm the baseline remains useful. With scripts
enabled, inspect listeners and mutations, activate by Enter and Space, and
compare the accessibility tree before and after every state change. Insert long
text, repeated clicks, missing elements, and a deliberate exception. Test
keyboard focus, screen-reader announcements, reduced motion, 200% zoom, and
320-pixel reflow. Search for innerHTML and justify every occurrence.
Exercise and next step
Enhance a static status panel with a button that changes one sentence through
textContent and exposes an accurate pressed or expanded state. Make
initialization tolerate the panel being absent. Then use the forms lesson to add
validation feedback without replacing native submission or server checks.
Practice with mutation recovery
Extend the disclosure so its text arrives after a delayed promise. Simulate success, rejection, a repeated click, and removal of the target before the promise settles. Keep the button's state accurate, avoid writing into a missing node, and announce only the final relevant status. Then disable the script and confirm the original summary and guidance still let the person continue. This exercise reveals race conditions without requiring a framework.
Inspect the final DOM after each outcome and verify that repeated initialization does not add duplicate listeners or duplicate IDs. Reload, use browser history, and revisit the component so cleanup and initialization behavior are observable. A small enhancement should remain deterministic when mounted once, mounted again, or skipped because its markup is absent.
Throttle the CPU and network for the final pass, then activate the control before initialization finishes. The interface must not accept an action it cannot honor or announce stale status after a newer result. Record the observed DOM and focus after each outcome so timing behavior becomes reproducible rather than an occasional manual surprise.