Skip to content
Explainers logo Explainers for Devs Helping you build a better Web

DOM Scripting

User interaction with JavaScript

This article is suitable for those who have just begun learning JavaScript and would like to explore some of the ways in which it can be used to add behaviour to web documents. In this example we explore how to make our document react to user actions employing a technique known as DOM Scripting.

Get the source code for this explainer.

Reading time:
18 minutes
On this page

Introduction

The code described in this explainer is designed to demonstrate how JavaScript (in conjunction with CSS and the DOM) can be used to change the styling of one element based on a user interaction with another element. It uses the fundamentals of DOM Scripting, a method for manipulating the Document Object Model with JavaScript, to:

  1. Select elements (nodes on the DOM tree) and assign them to variables.
  2. Add event listeners to selected elements.
  3. Manipulate the class attributes of selected elements.

If you learn how to do these three things and put them together in a logical script, you will be well on your way to learning most of what you need to work effectively with JavaScript as a front-end designer.

The traffic light widget

The widget below demonstrates a simple use for DOM Scripting, a user can change the visual styling of one element by interacting with another element. In this case, the user is provided with several options.

A Traffic Light Controller

Oops! - It looks like JavaScript isn’t available. Unfortunately, this widget won’t work without it.

In its default state, the light is turned off, and that’s what we see when the page first loads. Clicking or tapping on one of the colour buttons will change the state. The “Off” button will return the light to its default state.

As usual, the widget is built to web standards using the three technology layers of the web standards model: HTML, CSS, and JavaScript. Each of the three layers is independent of the others and exists in its own file. They are brought together only in the browser when the document loads, with the HTML file linking to the CSS file and to the JS file. Each layer is described below.

The HTML

The markup for the widget is very simple. It consists of a <section> element, which contains the whole widget, a heading <h3> (all sections should have a heading), an empty <div> styled to look like a light, and four <button> elements. It is appropriate to use section as the main container for our widget (rather than div) because the content stands alone and is separate from the main page content.


<section id="widget"> <!-- open widget -->
    <h3>A Traffic Light Controller</h3>
    <div></div> <!-- empty div for the light -->
    <button type="button">Red</button>
    <button type="button">Amber</button>
    <button type="button">Green</button>
    <button type="button">Off</button>
</section> <!-- close widget -->
			    
Code block 1

Each <button> is given a type attribute with a value of "button". This may seem odd, but it is required in this context because the default behaviour for the button element is to submit form data. In this case, we have nothing to submit, in fact, these buttons are not even part of a form. By adding this attribute and value, we cause the button to have no default behaviour (it does nothing when clicked). We then have the freedom to add an event listener and make the button do whatever we want it to do using JavaScript.

The CSS

We’ll start by styling the main container, the heading, and the empty <div>. The container is selected using the ID #widget. We could have avoided adding an ID in our markup by using an alternative method of selection such as the section:first-of-type pseudo-class selector to target the first section in the document. We could even have used a simple type selector section since our section is the only element of its type in the document. However, our widget is a unique element within the document so is seems sensible to mark it up as such <section id="widget">. Although this adds more markup to our HTML file, ID is a more specific selector that allows us to write less verbose CSS. As a bonus, it also means that the widget is portable and can be used in other documents without the styling affecting other <section> elements.

The child elements can then be selected using a basic descendent combinator — any <h3> that is a descendent (child, grandchild etc.) of #widget and any <div> with the same relationship. Since there is only one element of each type within the section, we can be sure we are targeting just the elements we want.


/* The traffic light container */
#widget {
    background-color: #000;
    text-align: center;
    max-width: 600px;
    padding: 1.5em 0;
    border-radius: 12px;
    margin: 2.0em auto 2.5em;
}
#widget h3 {
    color: #ddd;
    margin: 0.5em;
}
#widget div {
    height: 225px;
    width: 225px;
    border-radius: 50%;
    border: 4px solid #bbb;
    margin: 2.5em auto 4.0em;
    background: #333;
    background: radial-gradient(#444, #111);
}
			    
Code block 2

The declarations for the three elements are straightforward. We’re setting a max-width on the parent element of 600 pixels. That means it can be less than 600px in narrow viewports, but it will never be wider than 600px. We’re also setting margin: 2.0em auto 2.5em, which means that the top margin will be 2.0em, the bottom margin will be 2.5em, and the left and right margins will both be set automatically. This ensures that there will be some space above and below, and that it will be centred within the content column (“auto” means the browser will look at the available space, and set the left and right margins to half of that space, effectively centring the element).

We’re styling the <div> to look like a circular light, so we set the height and width to the same value, which creates a square, and then we set the border-radius to 50%, turning the square into a circle. We add a radial-gradient() to the background to give a dome effect, remembering to set a solid colour background: #333; as a fallback for older browsers that may not understand radial-gradient(). The fallback value comes first, followed by the preferred value. The fallback value is understood by all browsers. Using the principle of Progressive Enhancement, modern browsers that understand radial-gradient will use that in preference to the fallback value, while older browsers will simply ignore it.


/* Button styling */
#widget button {
    color: #fff;
    font-family: Roboto, Arial, Helvetica, sans-serif;
    font-weight: 300;
    font-size: 1.0em;
    width: 4.25em;
    margin: 0 0.5em 1.0em;
    padding: 0.5em;
    border: 1px solid #888;
    border-radius: 4px;
    background-color: #444;
    cursor: pointer;
}
#widget button:hover {
    background-color: #666;
}
#widget button:active {
    background-color: #c00;
}
#widget button:focus {
    outline: none;
    background-color: #367fff;
}
			    
Code block 3

Styling the buttons is a little more complicated because we must override all the system style settings, including the font. Consequently, there are a lot of declarations in the rule for the buttons, but nothing out of the ordinary. The only less common property is the cursor property, to which we give a value of pointer so that our cursor changes to a pointer when it is over the button.

In addition, we need to consider how the button appears in its various states; hover, active, and when it attains focus. In this case we are simply changing the background colour on all three states, :hover, :active, and :focus using the respective pseudo-class selectors. We are also removing the browser default outline on the focus state by setting outline: none. Best practice is to never remove the default styling on :focus unless it is being replaced with something else. In this case, we are replacing it with a background colour.


/* Light colour classes */
#widget div.stop {
    background: #f81b10;
    background: radial-gradient(#fa635b, #b70e05);
}
#widget div.get-ready {
    background: #f8bc19;
    background: radial-gradient(#fad266, #c18f06);
}
#widget div.go {
    background: #61a010;
    background: radial-gradient(#8ce916, #385d09);
}
			    
Code block 4

Finally we need a class rule for each of the three traffic light colours, red, amber and green, using the class names .stop, .get-ready, and .go respectively. These are the classes that we will get JavaScript to add to the <div> when the corresponding buttons are clicked. In each case we are using a radial gradient with a flat colour fallback for older browsers.

The JavaScript

The script that does all the work is in three parts. In the first part, we select all the elements we are working with, the <div> and the <button> elements (clicking one of the buttons will change the style of the div). In the second part, we add an event listener to each of the buttons so that JavaScript knows when they are clicked and what to do when that happens. The third part contains four functions, one for each button. Each function changes the class on the <div> element so that it is styled with the colour indicated on the corresponding button.

Part 1: Selecting elements


// Select the elements we need and assign them to variables
const light = document.querySelector('#widget div');
const buttons = document.querySelectorAll('#widget button');
			    
Code block 5

Each of the two statements declares a new variable (using the const keyword), one we’ve called “light” and the other we’ve called “buttons”. Note that const is a variable declaration, which makes the variable “read-only” (its value cannot be changed). We’re using const here (rather than let or var) because we know the value of the variable will not change, so we can use const to add a little extra security to our script.

In the first statement, we use the document.querySelector() method to select the div element, which gets assigned to the light variable. In the second statement we use the document.querySelectorAll() method to select all the buttons, which are assigned to the buttons variable. The equals symbol, = is the assignment operator. In this case, it assigns the result of the method to the corresponding variable.

These two methods allow us to select elements using the familiar CSS selector syntax. For example, we can use #widget div as the query string to select the <div> element, and this is identical to the selector syntax we would use to select the same element in CSS. The difference between the two is that querySelector will select only the first element in the document that matches the selection query whereas querySelectorAll will select all elements in the document that match the selection query. The querySelector() method returns a reference to a single node and the querySelectorAll() method returns a node list (similar to an array).

Part 2: Adding event listeners


// Add an event listener to each button
buttons[0].addEventListener('click', redLight);
buttons[1].addEventListener('click', amberLight);
buttons[2].addEventListener('click', greenLight);
buttons[3].addEventListener('click', offLight);
			    
Code block 6

Each of the four statements adds an event listener to one of the four buttons (known as the event target). Just as in an array, node lists begin with element zero. The first button in the buttons node list can therefore be referenced using an index of 0, buttons[0]. The second, third and fourth buttons are referenced using indexes of 1, 2, and 3 respectively.

Each button in turn gets an event listener. We use the addEventListener() method to do this. addEventListener requires two or more parameters to do its job. In our example we are providing two. The first is the “type” parameter, this indicates the type of event that is being listened for. In this case we use the keyword “click”. JavaScript will listen out for a click event on the button. The second parameter is the “listener”. In our script, the listener is the name of the function that we want to run when the event occurs. Notice that the 'click' keyword is in quotes (because it is a string) but the name of the function is not (it’s a function name, not a string), and the parameters are separated with a comma.

Part 3: Building the functions


// One function for each button
function redLight(){
    light.setAttribute('class', 'stop');
}

function amberLight(){
    light.setAttribute('class', 'get-ready');
}

function greenLight(){
    light.setAttribute('class', 'go');
}

function offLight(){
    if (light.hasAttribute('class')) {
        light.removeAttribute('class');
    }
}
			    
Code block 7

Each of our four functions begins with the function keyword followed by the name of our function. The function name is always followed by open/close parenthesis (), even if we have no parameters to pass into the function (as in this case). The statements within the function are enclosed in curly braces { }.

The first three functions are very similar, consisting of just a single statement each. They do a simple job; they set the value of the class attribute on the <div> element (referenced using the light variable) to the name of the class corresponding to the button that has been clicked. For example, when the “Red” button is clicked, the redLight() function is run. That function sets the class attribute value to “stop”. In our CSS, the rule for .stop sets the background on the <div> element to red.

All this is achieved using the setAttribute() method. setAttribute requires two parameters, a name/value pair. The first is the type of the attribute you want to set (in our case, it’s a class attribute), and the second is the attribute value (the class name). Note that we can use this method to set any attribute on any selected element, so it’s a powerful tool. The other thing worth noting is that setAttribute() will add the specified attribute if it doesn’t already exist, but it will simply update the value if the attribute does exist.

The fourth function is a little different because it returns the <div> element to its default state where it has no class attribute. We’re using an if clause to test if the <div> element has a class attribute, and if it does have one, we remove it. We’re using two methods to achieve this. The hasAttribute() method has one parameter, the name of the attribute we want to check for. If it returns a value of “true” (i.e. there is a class attribute), the statement between curly braces will run. That statement uses the removeAttribute() method to remove the attribute indicated in the parameter. So the offLight() function checks to see if there is a class attribute, if there is one, it is removed and if there isn’t one, nothing happens.

The three methods we’re using here; setAttribute, hasAttribute and removeAttribute provide an excellent toolkit for manipulating attributes and thereby changing the style of elements in our document.

What if JavaScript is unavailable?

When working with JavaScript, we must always consider what happens to the user experience if, for whatever reason, JavaScript is not available. In many cases, where JavaScript is used to progressively enhance the user experience (e.g. a non-critical visual effect), it may not matter if JavaScript is unavailable because the document will still operate perfectly well without it. In our example, that is not the case. If JavaScript is not available for this document, not only does the widget not work, but the user experience is impacted because the buttons will do nothing and that may cause frustration.

The question, therefore, is what can we do about it? Essentially, there are two possible approaches that we can use to alert the user to the problem. One simple but crude solution, and one enhanced solution.

The <noscript> element

The simplest solution is to use the <noscript> HTML element. Content within the noscript element will display only in the event of JavaScript not being available (hence the name). If JavaScript is available, the content will be hidden.

In the HTML example below, we have added a warning message within <noscript>, informing the user that JavaScript is not available and that the buttons referred to in the preceding paragraph (the prompt) will not work.


<p class="prompt">Use the buttons to change the colour of the traffic light</p>
<noscript><p class="warning"><strong>Oops! -</strong>  It looks like JavaScript isn’t available. Unfortunately, this widget won’t work without it.</p></noscript>
			    
Code block 8

The CSS for our messages is straightforward. We’re giving our prompt an off-white colour so it reads well on the black background, and we’re styling our warning with greater prominence (white text on a red background) so that it cannot be missed.


/* In case JavaScript is turned off */
.prompt {
    color: #ccc;
}
.warning {
    color: #fff;
    background-color: #c00;
    line-height: 1.4;
    width: 80%;
    padding: 8px 15px;
    margin: 0 auto;
}
			    
Code block 9

In many cases, that simple solution may be all that’s required, but in this case, there may still be some confusion because both messages are displayed at the same time, one prompting the user to try the buttons and a second message telling them that the buttons won’t work.

The user prompt and the warning
Figure 1: Use of the noscript element may result in confusion.

An improved user experience

Ideally, we’d remove the message prompting the user to try the buttons. That way we’d be left with a single unambiguous message. One way to do this is to have a single default message advising the user that the widget won’t work and then use JavaScript to change the message to a prompt when the page loads. If we do that, the default warning message will display if JavaScript is not available, but the normal user prompt will be displayed if JavaScript is available. There’s a nice logic to the way this works, and it provides a better user experience.


<!-- This warning is swapped for a prompt on page load -->
<p class="warning"><strong>Oops! -</strong>  It looks like JavaScript isn't available. Unfortunately, this widget won't work without it.</p>
			    
Code block 10

The HTML in Code block 10 shows the default message. Notice that we’re marking this up as a paragraph and not as a noscript element. The CSS is the same as for the noscript version. The JavaScript below does the work of transforming our warning into a prompt when the page loads.


// If JavaScript is available, update the message
const message = document.querySelector('p.warning');
message.setAttribute('class', 'prompt');
message.innerHTML = 'Use the buttons to change the colour of the traffic light';
			    
Code block 11

The script in Code block 11 achieves our aim using three statements. The first statement selects the paragraph, using the document.querySelector() method using a specific class selector as the query string p.warning (it selects the paragraph with a class of “warning”). The selected paragraph is then assigned to a variable called message. We can then use that variable to target the paragraph in the next two statements.

The second statement uses the setAttribute() method to change the value of the class attribute on the paragraph from “warning” to “prompt”. That deals with the styling of the paragraph because the .prompt class already exists in our CSS.

The third statement changes the innerHTML property of the paragraph, effectively replacing the default warning text with the standard user prompt. Note that innerHTML is a property of the selected element and not a method. That’s why the syntax of the third statement is different from that of the second statement.

The result of our efforts is that we now have an unambiguous warning and an improved user experience. Notice that we have placed the warning/prompt above the buttons (not below them). This ensures that users of screen readers will encounter the message before they get to the buttons. Again, this simple accessibility consideration could mean less frustration for some users.

Further improvements

We’ve already done a lot to improve the experience for our users in the event of JavaScript failing, but we could do even more with our new DOM scripting powers.

Currently, even if JavaScript is unavailable, the buttons on our widget look and act as though they work, even though they don’t. Clearly, that’s another opportunity for us to provide a better experience. Ideally, our buttons should be disabled if JavaScript isn’t available. We can do this using the same logic we used for the warning/prompt message. We’ll set our buttons to be disabled by default, and then enable them with JavaScript. That way, if JavaScript isn’t available, the buttons will remain disabled.

The first step is to change the HTML of our widget to disable the four buttons. We do this by adding the disabled attribute to each button. This attribute is a boolean (it’s either true or false), so it doesn’t require a value. If an element has the attribute, it is disabled if, it does not, it is enabled. Our revised HTML is shown in Code block 12.


<section id="widget"> <!-- open widget -->
    <h3>A Traffic Light Controller</h3>
	<!-- This warning is swapped for a prompt on page load -->
    <p class="warning"><strong>Oops! -</strong>  It looks like JavaScript isn't available. Unfortunately, this widget won't work without it.</p>
    <div></div> <!-- empty div for the light -->
    <button type="button" disabled>Red</button>
    <button type="button" disabled>Amber</button>
    <button type="button" disabled>Green</button>
    <button type="button" disabled>Off</button>
    <!-- buttons are enabled on page load -->
</section> <!-- close widget -->
			    
Code block 12

Once we have set the disabled attribute on our buttons, we need to style the button to look as though it is disabled. Typically, this is done by reducing the opacity of the button or reducing the saturation if the button is coloured. In this case, we’ll reduce opacity.


#widget button[disabled] {
    opacity: 0.35;
    cursor: not-allowed;
}
			    
Code block 13

We’re using an attribute selector to target any button within #widget that has the [disabled] attribute. Once the buttons are targeted in this way (they will only be selected if they are disabled), we can add our declarations. First, we set opacity to a value of 0.35 (the default value is 1.0). Effectively, this means we are setting the opacity to 35%. Additionally, we are changing the cursor style so that instead of seeing the usual pointer icon, we’ll see the not allowed icon to further reinforce the visual message that the buttons are disabled.

Finally, we need to add some JavaScript that will remove the disabled attribute from the buttons. Previously, when we wanted to remove an attribute, we used the removeAttribute() method, and we could do that here. However, in this case, the attribute we want to remove is a boolean attribute, so all we need to do is to change the disabled property to “false” for each button.


buttons.forEach(button => button.disabled = false);
			    
Code block 14

Fortunately, we can do this job with a single statement. Since we already have the buttons selected as a node list and assigned to a variable called buttons, we can use the forEach() method to step through the node list, one element at a time, and change the boolean value to “false” (effectively enabling the button). The forEach method is designed specifically for stepping through all elements in an array (a node list is like an array) and running a function on each element in turn. In our example, we are using the arrow function =>. This allows us to run a single expression on each of our buttons in a more concise way. The statement is basically saying, “do this thing to each button in the list of buttons”.

The warning message without a prompt
Figure 2: The user prompt is transformed into a warning and the buttons are disabled.

The result of all our hard work is a widget that works well when JavaScript is available, and that remains accessible and provides a satisfactory user experience when things go wrong. As is often the case, designing for errors and edge cases takes as much effort as designing the core functionality of a user interface, but it is a fundamental aspect of good design practice.

Getting more from your browser

You can test what happens to any document when JavaScript is not available by turning JavaScript off in your browser. For Firefox Developer Edition, go to Advanced Preferences (about:config in the address bar) and toggle the value of javascript.enabled to “false”.

The Firefox code inspector showing generated content
Figure 3: The Web Tools Inspector showing generated content in Firefox Developer Edition.

It’s worth noting that if you use your browser to view the source of this document, you will see the default warning message that we hard coded into our HTML file, but you can view the generated markup (the user prompt) using the Inspector in Web Tools. The Inspector always gives a live view of our code, with any changes to the DOM shown in real-time, whereas View Source shows only the static HTML file.

Top of page