- Lab
- Core Tech

Guided: Enhancing User Engagement with JavaScript
In this lab, you'll gain hands-on practice with JavaScript interaction fundamentals like event handling, implement practical features like drag-and-drop support, and learn how to animate your content with CSS managed by JS through the Web Animations API.

Path Info
Table of Contents
-
Challenge
Introduction
This lab will guide you through a series of small but widely applicable JavaScript implementations to enhance front-end experiences and boost user engagement. The lab content assumes you're already somewhat familiar with JavaScript, HTML, CSS, and the DOM.
Some notes:
- You'll do all your coding in
src/main.html
. Per best practices, it's recommended you keep strict mode enabled by leaving'use strict'
as the first line in the<script>
area. - You can click the refresh button in the Web Browser tab to see the effects of the changes you've made.
- Sometimes you'll replace your previous content in the same file. The first lines of the task will specify either way.
- If you get stuck on a task, you can consult the
solutions/
folder.
- You'll do all your coding in
-
Challenge
Event Binding
In web programming, an event is like when your phone chimes — it's a signal that something happened that may warrant some kind of action in response. Maybe your phone's "incoming text" chime plays, but you ignore it for the moment — making a mental note to check your texts later — because you're focusing on a code lab right now. Or maybe it rings because there's an incoming call, and you glance at the screen to see if it's that important call you're waiting for.
Just as you listen for these signals and respond a certain way depending on the type of chime, your web code can listen for specific events and take associated action when they occur. Instead of incoming texts or calls, web events are things like a timer going off, a CSS transition completing, a document finishing loading, or your end user pointing, clicking, or typing.
Consider the following HTML:
<div id="square"></div> <select id="mySelector"> <option>ABC</option> <option>DEF</option> <option>GHI</option> </select>
The
select
element automatically emits a built-in'change'
event when a user selects an option. (Another common way to say this: The event "fires.") In this case, there are several ways the user could do this. For example:- They can press Tab to put the keyboard focus on the element, then press Space or Enter. Or, they could click or tap the element once.
- Either way, a dropdown menu will appear, and they can use the mouse to hover, or use the arrow keys, to highlight an option. But the event doesn't fire yet.
- If they then hit Enter while an option is highlighted, the event fires.
- If they click or tap an option, the event fires.
- They can press Tab to put the keyboard focus on the element, then use the arrow keys. Each time an arrow keypress actually changes the current option, the event fires.
Often you only need to know when the current option selection changes and what it changes to, but you don't need to know exactly how the user changes it. Thankfully, when this is the case, you only need your code to listen for the
'change'
event.To that end, you need a function to handle the event whenever it fires. The only parameter the function takes is an event object, which has useful members like
target
. In this case,target
will be theselect
element, whosevalue
corresponds to that of the newly selectedoption
element:function handleSelectorChange(event) { document.getElementById('square').textContent = event.target.value }
Listening for and handling this event is as straightforward as passing the above function in a call to the element's
addEventListener
method:var select = document.getElementById('mySelector') select.addEventListener('change', handleSelectorChange)
Next, you'll use the above pattern to implement a selector for the background color of the square
<div>
element. Built-in events cover many common use cases, but you can also create, listen for, and "dispatch" (emit, fire) your own custom events.CustomEvents are created with the
CustomEvent
constructor, where you can specify an event name and whatever you like in thedetails
that they should carry to their event handlers:var colorChangeEvent = new CustomEvent('colorChange', { detail: { color: someColor }, })
Dispatching such an event is done with an element reference and its
dispatchEvent
method:someElement.dispatchEvent(colorChangeEvent)
The event name you use in the constructor is used (instead of
'change'
) to listen for it:someElement.addEventListener('colorChange', handleColorChange)
Next, you'll use this pattern to add a layer to the event flow of your previous task solution. Instead of changing the square's color directly, you'll use the original
'change'
event to, in turn, fire a custom'colorChange'
event to the square itself. You're right to wonder — if the code is longer and more involved, and the app behavior from a user's perspective is the same, why go to this trouble? Indeed, in simple situations like the scenario used for this task, it doesn't make sense to add a custom event layer.In more complicated situations, however, this approach is a way to identify events that are more specific to your app's context than built-in events are. For example, say you were building an interface that had not only a
select
element but also a color wheel and RGB sliders. If the handlers for each sub-interface change the square color and also have to update each other to stay in sync, there's a lot of duplication and accompanying maintenance burden. Instead the handlers can dispatch a'colorChange'
event. Better still, when some new requirement comes along — e.g., that user color selections should also be logged to a server somewhere — you can add a single new'colorChange'
listener for that, rather than bloating your existing centralized handler. - They can press Tab to put the keyboard focus on the element, then press Space or Enter. Or, they could click or tap the element once.
-
Challenge
Form Validation
Thanks to HTML5's built-in constraint validation semantics, form validation often doesn't require any JavaScript at all. Take the following form code, for example:
<form> <input type="password" id="new_password" minlength="8" required /> <input type="submit" value="Set new password" /> </form>
Given the
minlength
andrequired
attributes, browsers won't let the form be submitted without a password having at least eight characters. Better still, they'll automatically generate an appropriate pop-up when user input doesn't meet the specified requirements. It's straightforward to have form element styles change correspondingly:input:user-invalid { background-color: pink; }
Using the
:user-invalid
pseudo-class is similar to another built-in (:invalid
) but carries the advantage of waiting for the user to interact with the form field first. It avoids the ugly situation of a form being always initially full of "complaints" about data the user hasn't had a chance to input yet.You can do a lot with built-in constraints — even using regular expression pattern matching — but not everything. For example, suppose you would like to prevent users from entering a password that's known to be extremely common and therefore likely to be guessed by a malicious third party. Thankfully, you don't need to abandon the benefits of built-in validation: You can integrate this extra validation requirement seamlessly using the Constraint Validation API.
The heart of this mechanism lies in the built-in
setCustomValidity
method of a given form element. Pass it an empty string to signal that a field's input is valid according to your custom constraint, while still leaving the built-in constraints to do their own validation. Pass it any other string, and that's the message that will pop up for the user the next time they try to submit the form.For this particular validation against an array of common passwords, you can use the built-an
Array.includes
function like this:if (commonPasswords.includes(field.value)) // (exactly the way it reads)
Validation still happens in response to every keystroke if you use the
'input'
event rather than the'change'
event you saw earlier:passwordField.addEventListener('input', myHandler) // where myHandler calls setCustomValidity
No message will pop up until subsequent form submissions, but the pseudo-class
:user-invalid
will meanwhile indicate the live validity status of an element. If your validation used some particularly computation-heavy method on every keystroke, you could avoid it by returning early in cases where user input was failing built-in validations already:field.setCustomValidity('') if (!field.validity.valid) return
-
Challenge
Pointer Events
Form elements aren't the only use case for events, and events can get more fine-grained than you've seen with forms.
For example, suppose you're building a color selector, starting with this HTML structure:
<div id="main_square"></div> <div id="palette"> <div></div> <div></div> <div></div> </div>
You then give these
div
s some sizes and line the palette swatches up horizontally:#main_square { border: 1px solid black; width: 300px; height: 300px; } #palette > div { display: inline-block; width: 100px; height: 100px; }
You want to support both mobile and desktop users, and make it so that when they click, tap, hover over, or swipe through a swatch, the
main_square
changes color to match the event. Thankfully, you don't need to code the same logic multiple times to support touch surfaces, mice, and other pointing devices like stylus pens: You can use pointer events to support them all through a single interface.In this case, you need only add event listeners to the
palette
for'pointerdown'
and'pointermove'
, and they can both point to the same function. To get an idea of what data is exposed by pointer events, you decide to have this function also display in themain_square
some text indicating several properties of the pointer event it's handling:${pointerType}: ${pressure} @ ${x}, ${y}
.But then you notice that on mobile, you encounter difficulty dragging the touch pointer around the palette and the rest of the page. Because of the default way that phone browsers interpret such actions, you see unintentional navigation events (back, forward, reload) and/or scrolling, instead of behavior aligned with how the same page works on desktops/laptops with mice/trackpads.
To override this, your swatch style definition just needs one more line,
touch-action: none;
. There are some subtle differences between mouse and touch events that pointer events don't quite capture. For example, say your app needs special behavior when a user holds down a mouse button, then (without releasing the first button) clicks and holds a second (and even third) mouse button. In this situation, only one'pointerdown'
event will fire for the whole sequence, so you would need to use'mousedown'
events to handle this corner case.Another example is double-clicking — there is a mouse event for this (hooking into OS-level user preferences around the timing that constitutes a double-click) but no agreed-upon equivalent for touch events. (You could implement the logic for that manually if double-tap support were important to your use case.)
Both of these cases are uncommon: Pointer events will suffice for most single-pointer applications. What about multi-touch gestures?
You can modify the pointer demo you just created to see how they work. The biggest difference with multi-touch is that you have to manually track a list of
pointerId
values from event objects to even know how many currently active touch points there are. Every time a new touch begins, the DOM generates a newpointerId
; when an existing touch ends, that ID must be scratched from the list.Since multi-touch gestures will take more space, it will be better to listen to events on the whole
window
instead of just thepalette
div
.In the previous task, you had one handler for both
'pointerdown'
and'pointermove'
, but these must be separated for multi-touch. The previous logic can stay in the "move" handler, though the output being displayed in the square can be changed to show the active ID list. You'll then need thepointerId
-tracking logic in separate "down" and "up" handlers:var pointerIds = {} function pointerDownHandler(event) { pointerIds[event.pointerId] = 1 pointerMoveHandler(event) } function pointerUpHandler(event) { delete pointerIds[event.pointerId] pointerMoveHandler(event) }
Here, the
pointerIds
being an object allows quick access; the value of1
could be anything. (Alternatively, you could use a set.) Both functions callpointerMoveHandler
to reuse its now-separate display logic.There are several ways a pointer event can end (for example, dragging outside the device or window boundary, a pointing device becoming disconnected, or simply lifting a finger so it's no longer in contact with a touchscreen). In the context of this palette demo,
pointerUpHandler
can treat four different event types as equivalent and handle all of them:'pointerup'
,'pointerout'
,'pointerleave'
, and'pointercancel'
.Lastly, the previous
touch-action: none
use will work best if moved into a declaration forhtml
. But even then it won't suffice in preventing default multi-touch gestures like pinching and zooming; for that, you need to add the following header tag:<meta name="viewport" content="width=device-width, user-scalable=no" />
(The
user-scalable=no
is what helps, but because it prevents users from zooming,width=device-width
makes sure the interface is a decent size to keep the text readable.) This demo displays basic multi-touch debug output, but what about actual gestures? Since these vary by device and application, there's no built-in standard for them. In real-world multi-touch scenarios, you need to implement your own algorithm for gestures like pinching and zooming, or use a library like PinchZoom.js or panzoom that handles the algorithmic basics while allowing you to adjust parameters to suit your specific use case. -
Challenge
Drag and Drop
Unlike multi-touch gestures, there is straightforward, built-in support for drag-and-drop events. In fact, it's so built-in, even regular text on a web page already has functionality enabled by default — any text that you can select, you can subsequently drag to start a drag-and-drop event sequence:
<p><b>Normal</b> rich text you can only drag if you first select it.</p>
If you want to make a whole element (of nearly any kind) draggable, all it takes is the addition of one attribute:
<div id="main_square" draggable="true"> Some rich text, <b>draggable as a unit</b>. </div>
This already makes the
div
draggable. To also make it droppable, you need to have it listen for the'dragstart'
event, and handle it like this:function dragstartHandler(event) { var { dataTransfer, target } = event dataTransfer.setData('text/html', target.innerHTML) dataTransfer.setData('text/plain', target.textContent) }
The event's
dataTransfer
property is a special object that can hold one value for any particular MIME type. (I.e., if you addeddataTransfer.setData('text/html', '<b>something else</b>')
to the end of the above function, it would override the value for'text/html'
, not add to it.) Often it makes sense to set data for several similar MIME types, as this function shows.The app on the receiving end of this data (which may or may not be your app) can decide what to do with each part of the data, including ignoring MIME types it doesn't want to handle. You can create two drop zones to see this in action:
<div id="html_target" class="drop_zone"></div> <div id="text_target" class="drop_zone"></div>
Before these can be useful, you must prevent the default browser behavior for drops (just like you had to prevent certain touch event defaults in earlier tasks). I.e., browsers normally don't accept drops on most elements. All that's needed there is to call
event.preventDefault()
in the handler of a'dragover'
event.To actually do something with the dropped data, you need a separate handler for
'drop'
events. The API is very similar to drag handlers: You're given adataTransfer
property in the event, and it has agetData
method that takes a MIME type.It would be handy if users could drag your draggable elements and drop them elsewhere in their operating system. In some cases, they can: The above HTML snippet can be dropped into office apps, other types like
image/png
data might be droppable into some image-editing apps. On some Linux and Windows desktops, you can even droptext/plain
data into a file folder to automatically create a text file. But this isn't consistently supported and only works with plain text.So, to export data as a file in a consistently supported way, you need a workaround that uses blobs, which are file-like data storage objects. To create one, you put data in an array (even if you just have one piece of data) and pass that data to a
Blob
constructor along with an object specifying the MIME type under a key calledtype
:var sampleData = [document.getElementById('main_square').innerHTML], blob = new Blob(sampleData, { type: 'text/html' })
The more "workaround-feeling" part comes next. You need to create an anchor element (
<a>
), set itshref
to represent your blob viaURL.createObjectURL
, provide a suggested end-user filename, then click this link on the user's behalf — without ever making it visible to the user by adding it to the DOM:var a = document.createElement('a') a.href = URL.createObjectURL(blob) a.download = 'sample.html' a.click()
To make the process user-friendly, have this code live in the click handler of a button with a clear label like "Export." The subtle bug you may have noticed is that if you dropped onto the bold part of some previously-dropped text, it didn't replace the contents of the whole drop zone. That's because the event's
target
is the<b>
element, so it's only the bold element's contents that get replaced with your dragged text. The solution is to use the event'scurrentTarget
instead, which is the element the handler is registered on.About the
draggable
attribute — it can also be used to override the default value ("auto"
) in the other direction. So elements like images, which are draggable by default, can be made non-draggable withdraggable="false"
. -
Challenge
Animation
User engagement can also be enhanced with CSS transitions and animations. While both provide animation in a sense, animations are the more flexible of the two: They can loop and be more complex, whereas transitions are used more for simple, momentary effects. To see the difference, you can create two elements with the same content:
<div id="heart">♥</div> <div id="heart2">♥</div>
They can have some styling in common:
#heart, #heart2 { color: red; font-size: 10em; display: inline-block; }
To animate
#heart
with an animation, you need a set of keyframes, which define the states that CSS properties must be at different moments during the timeline of the animation. To make#heart
beat, you can animate it such that at 0% (the beginning of the timeline), it hastransform: scale(1)
CSS styling applied to it, then at 50% thescale
goes up to 1.2 (the heart gets 20% bigger), then at 100% (the end), it returns to normal size.@keyframes heartbeat { 0% { transform: scale(1); } /* implied */ 50% { transform: scale(1.2); } 100% { transform: scale(1); } /* implied */ }
Here, since 0% and 100% (synonyms:
from
andto
, respectively) both use the default CSStransform
property, they can be omitted, so your keyframe definition only needs to mention the 50% marker.To use this keyframe definition, you refer to it by the name you gave it above (
heartbeat
) in the CSSanimation
property:#heart { animation: 1s infinite heartbeat; }
The
animation
property is shorthand for up to nine properties. Here, you've specified the equivalent of:#heart { animation-duration: 1s; animation-iteration-count: infinite; animation-name: heartbeat; }
Since it loops, this same animation isn't possible as a transition, but here's something similar:
#heart2 { transition: transform 0.5s; } #heart2:hover { transform: scale(1.2); }
Here, the
:hover
pseudo-class specifies that#heart2
should be 20% bigger when the user mouses over the heart. Thetransition
property then specifies that any such effects should be applied smoothly over the course of a half-second. (If you moved thetransition
property to the pseudo-class instead, it would only be applied when the heart grows; the heart would suddenly return to normal size whenever hovering ends.) Just like you can use JavaScript to add CSS to an element, the Web Animations API adds the same ability for animations. As with styles, the semantics for animations differ slightly between JavaScript and CSS. Likewise, JavaScript lets you create animation specifications that are a bit more dynamic than what CSS allows. For example, you could add a click handler to the page that would make the heart rotate to point at the user's click location, which isn't possible in CSS.Another Web Animations API feature exposed specifically in JavaScript is the ability to pause, unpause, and skip to the end of animations — even when an element uses multiple animations.
While sequences aren't yet first-class citizens, multiple animations let you achieve a similar effect. Suppose you want your
heartbeat
keyframes not to loop forever, but instead be followed by another set of keyframes,heartspin
.If two time values are specified in the CSS
animation
property shorthand, the second will be interpreted as the delay to use before the animation starts. You can startheartbeat
right away, thenheartspin
can start after the duration ofheartbeat
. Just separate their definitions with a comma within the CSSanimation
property:animation: 1s 0s heartbeat, 2s 1s heartspin;
Here,
heartbeat
runs for 1 second after no delay, thenheartspin
runs for 2 seconds after waiting 1 second. And the0s
can be omitted, since starting right away is the default — the two shorthand definitions don't need to have completely parallel components.But what if you want to try running
heartbeat
for shorter or longer than 1 second? You'll need to change both instances of1s
. Thankfully, CSS variables can be used to eliminate the duplication::root { --beat-duration: 1s; } /* ... */ #heart { /* ... */ animation: var(--beat-duration) heartbeat, 2s var(--beat-duration) heartspin; }
While somewhat less readable, this approach is much more maintainable. Consider if you want
heartbeat
to run three times beforeheartspin
starts. You can define another variable,--beat-count
, then use thecalc()
feature of CSS to multiply the values of your variables together — i.e.calc(var(--beat-duration) * var(--beat-count))
— as the starting delay forheartspin
. The--beat-count
can also be included just beforeheartbeat
in the shorthand, to specify the number of loops.Though these animations apply to the same element, their timelines are technically independent — hence all the manual timing calculations. What this means when controlling such animations via the Web Animations API is that when you want to pause the heart, you'll have to pause both animations. Otherwise,
heartspin
will still run after the specified delay.To pause, you need to get the animations by calling the
getAnimations
method (without parameters) of the element in JavaScript. Then, for each animation in the array it returns, call the animation'spause
method (without parameters). The same can be done for unpausing viaplay
and skipping to the end viafinish
. The Web Animations API has another feature commonly used in animation playback, which is the ability to control playback speed. Animations have aupdatePlaybackRate
method for this. It takes a number as its only parameter as a multiplier for the playback speed (with 0 meaning stopped, though this is internally different from the paused state you get by calling thepause
method).Though some video playback interfaces use a drop-down menu with set choices (0.5x, 1x, 2x, etc.), it's possible to give more fine-grained control to your users using a
range
input
element:<input type="range" id="rateSlider" />
By default, this will have a
value
property consisting of an integer from 0-100, which will be useful in handling the element's'input'
event. However, you probably don't want to pass that along toupdatePlaybackRate
unmodified — most users won't want to watch a five-second animation at 100 times its normal speed. To limit the output range to numbers between 0.3 and 3.0, and also ensure finer-grained control on the slower end of the spectrum, you can use this formula in your event handler (assuming its parameter is namedevent
) to find the appropriate value to callupdatePlaybackRate
with:0.3 * 10 ** (event.target.value / 100)
Here, the
**
operator means "to the power of." So from input values of 0-100, this gives values from 0.3 (because 10 to the power of0/100
is 1) to 3.0 (because 10 to the power of100/100
is 10), smoothed exponentially thanks to the**
operator.Note that, even though this
input
element is outside a<form>
, some browsers will remember its last value between page loads. This is true even if you explicitly set it in HTML withvalue="50"
. This may or may not be what you want for an animation player, but in the context of this lab, you'll need to force its starting value on page load by setting it via JavaScript.Likewise, because the animation is relatively short, it's helpful for manual testing purposes to have all animations start in a paused state. To do this, you can prepend
paused
to each animation definition, e.g.animation: paused ..., paused ...
. One last thing: In other contexts you may need to get your element references and add event listeners to them within a'DOMContentLoaded'
event handler added to thewindow
object:window.addEventListener('DOMContentLoaded', function() { var xyz = document.getElementById('xyz') xyz.addEventListener('dragstart', dragstartHandler) })
In this lab you didn't need this technique, thanks to the scripts being defined last within the HTML file.
--
Congratulations on completing this lab!
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.