Did you know that DOM elements with IDs are accessible in JavaScript as global variables? It’s one of those things that’s been around, like, forever but I’m really digging into it for the first time.
If this is the first time you’re hearing about it, brace yourself! We can see it in action simply by adding an ID to an element in HTML:
<div id="cool"></div>
Normally, we’d define a new variable using querySelector("#cool") or getElementById("cool") to select that element:
var el = querySelector("#cool");
But we actually already have access to #cool without that rigamorale:
So, any id — or name attribute, for that matter — in the HTML can be accessed in JavaScript using window[ELEMENT_ID]. Again, this isn’t exactly “new” but it’s really uncommon to see.
As you may guess, accessing the global scope with named references isn’t the greatest idea. Some folks have come to call this the “global scope polluter.” We’ll get into why that is, but first…
Internet Explorer was the first to implement the feature. All other browsers added it as well. Gecko was the only browser at the time to not support it directly in standards mode, opting instead to make it an experimental feature. There was hesitation to implement it at all, but it moved ahead in the name of browser compatibility (Gecko even tried to convince WebKit to move it out of standards mode) and eventually made it to standards mode in Firefox 14.
One thing that might not be well known is that browsers had to put in place a few precautionary measures — with varying degrees of success — to ensure generated globals don’t break the webpage. One such measure is…
Variable shadowing
Probably the most interesting part of this feature is that named element references don’t shadow existing global variables. So, if a DOM element has an id that is already defined as a global, it won’t override the existing one. For example:
<div id="foo">I will be overridden :(</div> <script> window.foo = "bar"; console.log(window.foo); // Prints "bar" </script>
This behavior is essential because it nullifies dangerous overrides such as <div id="alert" />, which would otherwise create a conflict by invalidating the alert API. This safeguarding technique may very well be the why you — if you’re like me — are learning about this for the first time.
The case against named globals
Earlier, I said that using global named elements as references might not be the greatest idea. There are lots of reasons for that, which TJ VanToll has covered nicely over at his blog and I will summarize here:
If the DOM changes, then so does the reference. That makes for some really “brittle” (the spec’s term for it) code where the separation of concerns between HTML and JavaScript might be too much.
Accidental references are far too easy. A simple typo may very well wind up referencing a named global and give you unexpected results.
It is implemented differently in browsers. For example, we should be able to access an anchor with an id — e.g. <a id="cool"> — but some browsers (namely Safari and Firefox) return a ReferenceError in the console.
It might not return what you think. According to the spec, when there are multiple instances of the same named element in the DOM — say, two instances of <div class="cool"> — the browser should return an HTMLCollection with an array of the instances. Firefox, however, only returns the first instance. Then again, the spec says we ought to use one instance of an id in an element’s tree anyway. But doing so won’t stop a page from working or anything like that.
Let’s say we chuck the criticisms against using named globals and use them anyway. It’s all good. But there are some things you might want to consider as you do.
Polyfills
As edge-case-y as it may sound, these types of global checks are a typical setup requirement for polyfills. Check out the following example where we set a cookie using the new CookieStore API, polyfilling it on browsers that don’t support it yet:
<body> <img id="cookieStore"></img> <script> // Polyfill the CookieStore API if not yet implemented. // https://developer.mozilla.org/en-US/docs/Web/API/CookieStore if (!window.cookieStore) { window.cookieStore = myCookieStorePolyfill; } cookieStore.set("foo", "bar"); </script> </body>
This code works perfectly fine in Chrome, but throws the following error in Safari.:
TypeError: cookieStore.set is not a function
Safari lacks support for the CookieStore API as of this writing. As a result, the polyfill is not applied because the img element ID creates a global variable that clashes with the cookieStore global.
JavaScript API updates
We can flip the situation and find yet another issue where updates to the browser’s JavaScript engine can break a named element’s global references.
That script grabs a reference to the input element and invokes focus() on it. It works correctly. Still, we don’t know how long it will continue to work.
You see, the global variable we’re using to reference the input element will stop working as soon as browsers start supporting the BarcodeDetector API. At that point, the window.BarcodeDetector global will no longer be a reference to the input element and .focus() will throw a “window.BarcodeDetector.focus is not a function” error.
Bonus: Not all named elements generate global references
Want to hear something funny? To add insult to the injury, named elements are accessible as global variables only if the names contain nothing but letter. Browsers won’t create a global reference for an element with a ID that contains special characters and numbers, like hello-world and item1.
Conclusion
Let’s sum up how we got here:
All major browsers automatically create global references to each DOM element with an id (or, in some cases, a name attribute).
Accessing these elements through their global references is unreliable and potentially dangerous. Use querySelector or getElementById instead.
Since global references are generated automatically, they may have some side effects on your code. That’s a good reason to avoid using the id attribute unless you really need it.
At the end of the day, it’s probably a good idea to avoid using named globals in JavaScript. I quoted the spec earlier about how it leads to “brittle” code, but here’s the full text to drive the point home:
As a general rule, relying on this will lead to brittle code. Which IDs end up mapping to this API can vary over time, as new features are added to the web platform, for example. Instead of this, use document.getElementById() or document.querySelector().
I think the fact that the HTML spec itself recommends to staying away from this feature speaks for itself.
Being able to understand Node continues to be an important skill if you’re a front-end developer. Deno has arrived as another way to run JavaScript outside the browser, but the huge ecosystem of tools and software built with Node mean it’s not going anywhere anytime soon.
If you’ve mainly written JavaScript that runs in the browser and you’re looking to get more of an understanding of the server side, many articles will tell you that Node JavaScript is a great way to write server-side code and capitalize on your JavaScript experience.
I agree, but there are a lot of challenges jumping into Node.js, even if you’re experienced at authoring client-side JavaScript. This article assumes you’ve got Node installed, and you’ve used it to build front-end apps, but want to write your own APIs and tools using Node.
For a beginners explanation of Node and npm you can check out Jamie Corkhill’s “Getting Started With Node” on Smashing Magazine.
Asynchronous JavaScript
We don’t need to write a whole lot of asynchronous code on the browser. The most common usage of asynchronous code on the browser is fetching data from an API using fetch (or XMLHttpRequest if you’re old-school). Other uses of async code might include using setInterval, setTimeout, or responding to user input events, but we can get pretty far writing JavaScript UI without being asynchronous JavaScript geniuses.
If you’re using Node, you will nearly always be writing asynchronous code. From the beginning, Node has been built to leverage a single-threaded event loop using asynchronous callbacks. The Node team blogged in 2011 about how “Node.js promotes an asynchronous coding style from the ground up.” In Ryan Dahl’s talk announcing Node.js in 2009, he talks about the performance benefits of doubling down on asynchronous JavaScript.
The asynchronous-first style is part of the reason Node gained popularity over other attempts at server-side JavaScript implementations such as Netscape’s application servers or Narwhal. However, being forced to write asynchronous JavaScript might cause friction if you aren’t ready for it.
Setting up an example
Let’s say we’re writing a quiz app. We’re going to allow users to build quizes out of multichoice questions to test their friends’ knowledge. You can find a more complete version of what we’ll build at this GitHub repo. You could also clone the entire front-end and back-end to see how it all fits together, or you can take a look at this CodeSandbox (run npm run start to fire it up) and get an idea of what we’re making from there.
The quizzes in our app will consist of a bunch of questions, and each of these questions will have a number of answers to choose from, with only one answer being correct.
We can hold this data in an SQLite database. Our database will contain:
A table for quizzes with two columns:
an integer ID
a text title
A table for questions with three columns:
an integer ID
body text
An integer reference matching the ID of the quiz each question belongs to
A table for answers with four columns:
an integer ID
body text
whether the answer is correct or not
an integer reference matching the ID of the question each answer belongs to
SQLite doesn’t have a boolean data type, so we can hold whether an answer is correct in an integer where 0 is false and 1 is true.
First, we’ll need to initialize npm and install the sqlite3 npm package from the command line:
npm init -y npm install sqlite3
This will create a package.json file. Let’s edit it and add:
"type":"module"
To the top-level JSON object. This will allow us to use modern ES6 module syntax. Now we can create a node script to set up our tables. Let’s call our script migrate.js.
// migrate.js import sqlite3 from "sqlite3"; let db = new sqlite3.Database("quiz.db"); db.serialize(function () { // Setting up our tables: db.run("CREATE TABLE quiz (quizid INTEGER PRIMARY KEY, title TEXT)"); db.run("CREATE TABLE question (questionid INTEGER PRIMARY KEY, body TEXT, questionquiz INTEGER, FOREIGN KEY(questionquiz) REFERENCES quiz(quizid))"); db.run("CREATE TABLE answer (answerid INTEGER PRIMARY KEY, body TEXT, iscorrect INTEGER, answerquestion INTEGER, FOREIGN KEY(answerquestion) REFERENCES question(questionid))"); // Create a quiz with an id of 0 and a title "my quiz" db.run("INSERT INTO quiz VALUES(0,"my quiz")"); // Create a question with an id of 0, a question body // and a link to the quiz using the id 0 db.run("INSERT INTO question VALUES(0,"What is the capital of France?", 0)"); // Create four answers with unique ids, answer bodies, an integer for whether // they're correct or not, and a link to the first question using the id 0 db.run("INSERT INTO answer VALUES(0,"Madrid",0, 0)"); db.run("INSERT INTO answer VALUES(1,"Paris",1, 0)"); db.run("INSERT INTO answer VALUES(2,"London",0, 0)"); db.run("INSERT INTO answer VALUES(3,"Amsterdam",0, 0)"); }); db.close();
I’m not going to explain this code in detail, but it creates the tables we need to hold our data. It will also create a quiz, a question, and four answers, and store all of this in a file called quiz.db. After saving this file, we can run our script from the command line using this command:
node migrate.js
If you like, you can open the database file using a tool like DB Browser for SQLite to double check that the data has been created.
Changing the way you write JavaScript
Let’s write some code to query the data we’ve created.
Create a new file and call it index.js .To access our database, we can import sqlite3, create a new sqlite3.Database, and pass the database file path as an argument. On this database object, we can call the get function, passing in an SQL string to select our quiz and a callback that will log the result:
// index.js import sqlite3 from "sqlite3"; let db = new sqlite3.Database("quiz.db"); db.get(`SELECT * FROM quiz WHERE quizid = 0`, (err, row) => { if (err) { console.error(err.message); } console.log(row); db.close(); });
Running this should print { quizid: 0, title: 'my quiz' } in the console.
How not to use callbacks
Now let’s wrap this code in a function where we can pass the ID in as an argument; we want to access any quiz by its ID. This function will return the database row object we get from db.
Here’s where we start running into trouble. We can’t simply return the object inside of the callback we pass to db and walk away. This won’t change what our outer function returns. Instead, you might think we can create a variable (let’s call it result) in the outer function and reassign this variable in the callback. Here is how we might attempt this:
// index.js // Be warned! This code contains BUGS import sqlite3 from "sqlite3"; function getQuiz(id) { let db = new sqlite3.Database("quiz.db"); let result; db.get(`SELECT * FROM quiz WHERE quizid = ?`, [id], (err, row) => { if (err) { return console.error(err.message); } db.close(); result = row; }); return result; } console.log(getQuiz(0));
If you run this code, the console log will print out undefined! What happened?
We’ve run into a disconnect between how we expect JavaScript to run (top to bottom), and how asynchronous callbacks run. The getQuiz function in the above example runs like this:
We declare the result variable with let result;. We haven’t assigned anything to this variable so its value is undefined.
We call the db.get() function. We pass it an SQL string, the ID, and a callback. But our callback won’t run yet! Instead, the SQLite package starts a task in the background to read from the quiz.db file. Reading from the file system takes a relatively long time, so this API lets our user code move to the next line while Node.js reads from the disk in the background.
Our function returns result. As our callback hasn’t run yet, result still holds a value of undefined.
SQLite finishes reading from the file system and runs the callback we passed, closing the database and assigning the row to the result variable. Assigning this variable makes no difference as the function has already returned its result.
Passing in callbacks
How do we fix this? Before 2015, the way to fix this would be to use callbacks. Instead of only passing the quiz ID to our function, we pass the quiz ID and a callback which will receive the row object as an argument.
Here’s how this looks:
// index.js import sqlite3 from "sqlite3"; function getQuiz(id, callback) { let db = new sqlite3.Database("quiz.db"); db.get(`SELECT * FROM quiz WHERE quizid = ?`, [id], (err, row) => { if (err) { console.error(err.message); } else { callback(row); } db.close(); }); } getQuiz(0,(quiz)=>{ console.log(quiz); });
That does it. It’s a subtle difference, and one that forces you to change the way your user code looks, but it means now our console.log runs after the query is complete.
Callback hell
But what if we need to do multiple consecutive asynchronous calls? For instance, what if we were trying to find out which quiz an answer belonged to, and we only had the ID of the answer.
First, I’m going to refactor getQuiz to a more general get function, so we can pass in the table and column to query, as well as the ID:
Unfortunately, we are unable to use the (more secure) SQL parameters for parameterizing the table name, so we’re going to switch to using a template string instead. In production code you would need to scrub this string to prevent SQL injection.
function get(params, callback) { // In production these strings should be scrubbed to prevent SQL injection const { table, column, value } = params; let db = new sqlite3.Database("quiz.db"); db.get(`SELECT * FROM $ {table} WHERE $ {column} = $ {value}`, (err, row) => { callback(err, row); db.close(); }); }
Another issue is that there might be an error reading from the database. Our user code will need to know whether each database query has had an error; otherwise it shouldn’t continue querying the data. We’ll use the Node.js convention of passing an error object as the first argument of our callback. Then we can check if there’s an error before moving forward.
Let’s take our answer with an id of 2 and check which quiz it belongs to. Here’s how we can do this with callbacks:
// index.js import sqlite3 from "sqlite3"; function get(params, callback) { // In production these strings should be scrubbed to prevent SQL injection const { table, column, value } = params; let db = new sqlite3.Database("quiz.db"); db.get(`SELECT * FROM $ {table} WHERE $ {column} = $ {value}`, (err, row) => { callback(err, row); db.close(); }); } get({ table: "answer", column: "answerid", value: 2 }, (err, answer) => { if (err) { console.log(err); } else { get( { table: "question", column: "questionid", value: answer.answerquestion }, (err, question) => { if (err) { console.log(err); } else { get( { table: "quiz", column: "quizid", value: question.questionquiz }, (err, quiz) => { if (err) { console.log(err); } else { // This is the quiz our answer belongs to console.log(quiz); } } ); } } ); } });
Woah, that’s a lot of nesting! Every time we get an answer back from the database, we have to add two layers of nesting — one to check for an error, and one for the next callback. As we chain more and more asynchronous calls our code gets deeper and deeper.
We could partially prevent this by using named functions instead of anonymous functions, which would keep the nesting lower, but make our code our code less concise. We’d also have to think of names for all of these intermediate functions. Thankfully, promises arrived in Node back in 2015 to help with chained asynchronous calls like this.
Promises
Wrapping asynchronous tasks with promises allows you to prevent a lot of the nesting in the previous example. Rather than having deeper and deeper nested callbacks, we can pass a callback to a Promise’s then function.
First, let’s change our get function so it wraps the database query with a Promise:
// index.js import sqlite3 from "sqlite3"; function get(params) { // In production these strings should be scrubbed to prevent SQL injection const { table, column, value } = params; let db = new sqlite3.Database("quiz.db"); return new Promise(function (resolve, reject) { db.get(`SELECT * FROM $ {table} WHERE $ {column} = $ {value}`, (err, row) => { if (err) { return reject(err); } db.close(); resolve(row); }); }); }
Now our code to search for which quiz an answer is a part of can look like this:
That’s a much nicer way to handle our asynchronous code. And we no longer have to individually handle errors for each call, but can use the catch function to handle any errors that happen in our chain of functions.
We still need to write a lot of callbacks to get this working. Thankfully, there’s a newer API to help! When Node 7.6.0 was released, it updated its JavaScript engine to V8 5.5 which includes the ability to write ES2017 async/await functions.
When you have a function that returns a Promise, you can use the await keyword before calling it, and it will prevent your code from moving to the next line until the Promise is resolved. As we’ve already refactored the get() function to return a promise, we only need to change our user-code:
This looks much more familiar to code that we’re used to reading. Just this year, Node released top-level await. This means we can make this example even more concise by removing the printQuizFromAnswer() function wrapping our get() function calls.
Now we have concise code that will sequentially perform each of these asynchronous tasks. We would also be able to simultaneously fire off other asynchronous functions (like reading from files, or responding to HTTP requests) while we’re waiting for this code to run. This is the benefit of all the asynchronous style.
As there are so many asynchronous tasks in Node, such as reading from the network or accessing a database or filesystem. It’s especially important to understand these concepts. It also has a bit of a learning curve.
Using SQL to its full potential
There’s an even better way! Instead of having to worry about these asynchronous calls to get each piece of data, we could use SQL to grab all the data we need in one big query. We can do this with an SQL JOIN query:
// index.js import sqlite3 from "sqlite3"; function quizFromAnswer(answerid, callback) { let db = new sqlite3.Database("quiz.db"); db.get( `SELECT *,a.body AS answerbody, ques.body AS questionbody FROM answer a INNER JOIN question ques ON a.answerquestion=ques.questionid INNER JOIN quiz quiz ON ques.questionquiz = quiz.quizid WHERE a.answerid = ?;`, [answerid], (err, row) => { if (err) { console.log(err); } callback(err, row); db.close(); } ); } quizFromAnswer(2, (e, r) => { console.log(r); });
This will return us all the data we need about our answer, question, and quiz in one big object. We’ve also renamed each body column for answers and questions to answerbody and questionbody to differentiate them. As you can see, dropping more logic into the database layer can simplify your JavaScript (as well as possibly improve performance).
If you’re using a relational database like SQLite, then you have a whole other language to learn, with a whole lot of different features that could save time and effort and increase performance. This adds more to the pile of things to learn for writing Node.
Node APIs and conventions
There are a lot of new node APIs to learn when switching from browser code to Node.js.
Any database connections and/or reads of the filesystem use APIs that we don’t have in the browser (yet). We also have new APIs to set up HTTP servers. We can make checks on the operating system using the OS module, and we can encrypt data with the Crypto module. Also, to make an HTTP request from node (something we do in the browser all the time), we don’t have a fetch or XMLHttpRequest function. Instead, we need to import the https module. However, a recent pull request in the node.js repository shows that fetch in node appears to be on the way! There are still many mismatches between browser and Node APIs. This is one of the problems that Deno has set out to solve.
We also need to know about Node conventions, including the package.json file. Most front-end developers will be pretty familiar with this if they’ve used build tools. If you’re looking to publish a library, the part you might not be used to is the main property in the package.json file. This property contains a path that will point to the entry-point of the library.
There are also conventions like error-first callbacks: where a Node API will take a callback which takes an error as the first argument and the result as the second argument. You could see this earlier in our database code and below using the readFile function.
import fs from 'fs'; fs.readFile('myfile.txt', 'utf8' , (err, data) => { if (err) { console.error(err) return } console.log(data) })
Different types of modules
Earlier on, I casually instructed you to throw "type":"module" in your package.json file to get the code samples working. When Node was created in 2009, the creators needed a module system, but none existed in the JavaScript specification. They came up with Common.js modules to solve this problem. In 2015, a module spec was introduced to JavaScript, causing Node.js to have a module system that was different from native JavaScript modules. After a herculean effort from the Node team we are now able to use these native JavaScript modules in Node.
Unfortunately, this means a lot of blog posts and resources will be written using the older module system. It also means that many npm packages won’t use native JavaScript modules, and sometimes there will be libraries that use native JavaScript modules in incompatible ways!
Other concerns
There are a few other concerns we need to think about when writing Node. If you’re running a Node server and there is a fatal exception, the server will terminate and will stop responding to any requests. This means if you make a mistake that’s bad enough on a Node server, your app is broken for everyone. This is different from client-side JavaScript where an edge-case that causes a fatal bug is experienced by one user at a time, and that user has the option of refreshing the page.
Security is something we should already be worried about in the front end with cross-site scripting and cross-site request forgery. But a back-end server has a wider surface area for attacks with vulnerabilities including brute force attacks and SQL injection. If you’re storing and accessing people’s information with Node you’ve got a big responsibility to keep their data safe.
Conclusion
Node is a great way to use your JavaScript skills to build servers and command line tools. JavaScript is a user-friendly language we’re used to writing. And Node’s async-first nature means you can smash through concurrent tasks quickly. But there are a lot of new things to learn when getting started. Here are the resources I wish I saw before jumping in:
Every now and then, I find that I’ve accumulated a bunch of links about various things I find interesting. Like React and JavaScript! Here’s a list of nine links to other articles about them that I’ve been saving up and think are worth sharing.
Seed Funding for Remix Remix went open source after taking funding which seems like a solid move. It’s a for-now-React-only framework, so I think it’s fair that everyone asks how does it compare to Next.js. Which they answered. Probably worth noting again for us CSS folks, Kent mentioned: “Because Remix allows me to easily control which of my CSS files is on the page at any given time, I don’t have all the problems that triggered the JavaScript community to invent workarounds like CSS-in-JS.”
React Router v6 Speaking of that gang, they released React Router v6, which looks like a positive move — all hooks based, 50% smaller than v5 — but is yet another major version with API changes. React Router has a history of API changes like this and they trigger plenty of grumbling in the community. There is plenty of that again.
React Aria “A library of React Hooks that provides accessible UI primitives for your design system”from… Adobe. Interesting. Looks like some pretty hard problems being solved here, like FocusScope (“When the contain prop is set, focus is contained within the scope.”) and interesting color inputs, like useColorField, useColorSlider, and useColorWheel. There are 59 hooks in all, ranging from interactions and forms to overlays and internationalization, with plenty of others in between.
Front End Tables: Sorting, Filtering, and Pagination Tania Rascia: “One thing I’ve had to do at every job I’ve had is implement a table on the front end of an application that has sorting, filtering, and pagination.” No shame in reaching for a big library with all these features, but sometimes it’s best to DIY.
Good advice on JSX conditionals Vladimir Klepov covers the (weirdly) many ways fairly simple conditionals can go wrong, like the number 0 leaking into your markup, and how to manage update versus remount in conditionals.
useProseMirror I’ve found ProseMirror to be a pretty nice rich text editor in the past. The library itself isn’t actually in React, so I think it’s a smart call here to make a modern React wrapper for it.
Spead up sluggish inputs with useDeferredValue You can introduce gnarly input delay the more work that an onChange function has to do on a text input. “useDeferredValue gives us a way to separate high priority updates from low priority updates for cases like this.”
🎥 A Cartoon Intro to WebAssembly If you don’t have a good understanding of what WebAssembly is, then Lin Clark will get you there in this video from JSConf EU 2017. So, no, not a new link or anything, but it’s new to me!
🎥 Turborepo Demo and Walkthrough Vercel bought Turborepo. Turborepo is specifically focused on making monorepos better. As someone who’s main codebase is a monorepo with Lerna and Yarn Workspaces such that we can have multiple different sites all share things like a design system, this is right up our alley. This video is with the Turborepo creator Jared Palmer and Lee Robinson, head of developer relations at Vercel. In this video, you get to see it all work.
You know how there are JavaScript dialogs for alerting, confirming, and prompting user actions? Say you want to replace JavaScript dialogs with the new HTML dialog element.
Let me explain.
I recently worked on a project with a lot of API calls and user feedback gathered with JavaScript dialogs. While I was waiting for another developer to code the <Modal /> component, I used alert(), confirm() and prompt() in my code. For instance:
Then it hit me: you get a lot of modal-related features for free with alert(), confirm(), and prompt() that often go overlooked:
It’s a true modal. As in, it will always be on top of the stack — even on top of that <div> with z-index: 99999;.
It’s accessible with the keyboard. Press Enter to accept and Escape to cancel.
It’s screen reader-friendly. It moves focus and allows the modal content to be read aloud.
It traps focus. Pressing Tab will not reach any focusable elements on the main page, but in Firefox and Safari it does indeed move focus to the browser UI. What’s weird though is that you can’t move focus to the “accept” or “cancel” buttons in any browser using the Tab key.
It supports user preferences. We get automatic light and dark mode support right out of the box.
It pauses code-execution., Plus, it waits for user input.
These three JavaScripts methods work 99% of the time when I need any of these functionalities. So why don’t I — or really any other web developer — use them? Probably because they look like system errors that cannot be styled. Another big consideration: there has been movement toward their deprecation. First removal from cross-domain iframes and, word is, from the web platform entirely, although it also sounds like plans for that are on hold.
With that big consideration in mind, what are alert(), confirm() and prompt() alternatives do we have to replace them? You may have already heard about the <dialog> HTML element and that’s what I want to look at in this article, using it alongside a JavaScript class.
It’s impossible to completely replace Javascript dialogs with identical functionality, but if we use the showModal() method of <dialog> combined with a Promise that can either resolve (accept) or reject (cancel) — then we have something almost as good. Heck, while we’re at it, let’s add sound to the HTML dialog element — just like real system dialogs!
If you’d like to see the demo right away, it’s here.
A dialog class
First, we need a basic JavaScript Class with a settings object that will be merged with the default settings. These settings will be used for all dialogs, unless you overwrite them when invoking them (but more on that later).
The road for browsers to support <dialog> has been long. Safari picked it up pretty recently. Firefox even more recently, though not the <form method="dialog"> part. So, we need to add type="button" to the “Accept” and “Cancel” buttons we’re mimicking. Otherwise, they’ll POST the form and cause a page refresh and we want to avoid that.
So far, this.elements.accept is a reference to the “Accept” button, and this.elements.cancel refers to the “Cancel” button.
Button attributes
For screen readers, we need an aria-labelledby attribute pointing to the ID of the tag that describes the dialog — that’s the <legend> tag and it will contain the message.
Good news! The HTML dialog element has a built-in cancel() method making it easier to replace JavaScript dialogs calling the confirm() method. Let’s emit that event when we click the “Cancel” button:
That’s the framework for our <dialog> to replace alert(), confirm(), and prompt().
Polyfilling unsupported browsers
We need to hide the HTML dialog element for browsers that do not support it. To do that, we’ll wrap the logic for showing and hiding the dialog in a new method, toggle():
toggle(open = false) { if (this.dialogSupported && open) this.dialog.showModal() if (!this.dialogSupported) { document.body.classList.toggle(this.settings.bodyClass, open) this.dialog.hidden = !open /* If a `target` exists, set focus on it when closing */ if (this.elements.target && !open) { this.elements.target.focus() } } } /* Then call it at the end of `init`: */ this.toggle()
Keyboard navigation
Next up, let’s implement a way to trap focus so that the user can tab between the buttons in the dialog without inadvertently exiting the dialog. There are many ways to do this. I like the CSS way, but unfortunately, it’s unreliable. Instead, let’s grab all focusable elements from the dialog as a NodeList and store it in this.focusable:
Next, we’ll add a keydown event listener, handling all our keyboard navigation logic:
this.dialog.addEventListener('keydown', e => { if (e.key === 'Enter') { if (!this.dialogSupported) e.preventDefault() this.elements.accept.dispatchEvent(new Event('click')) } if (e.key === 'Escape') this.dialog.dispatchEvent(new Event('cancel')) if (e.key === 'Tab') { e.preventDefault() const len = this.focusable.length - 1; let index = this.focusable.indexOf(e.target); index = e.shiftKey ? index-1 : index+1; if (index < 0) index = len; if (index > len) index = 0; this.focusable[index].focus(); } })
For Enter, we need to prevent the <form> from submitting in browsers where the <dialog> element is unsupported. Escape will emit a cancel event. Pressing the Tab key will find the current element in the node list of focusable elements, this.focusable, and set focus on the next item (or the previous one if you hold down the Shift key at the same time).
Displaying the <dialog>
Now let’s show the dialog! For this, we need a small method that merges an optional settings object with the default values. In this object — exactly like the default settings object — we can add or change the settings for a specific dialog.
open(settings = {}) { const dialog = Object.assign({}, this.settings, settings) this.dialog.className = dialog.dialogClass || '' /* set innerText of the elements */ this.elements.accept.innerText = dialog.accept this.elements.cancel.innerText = dialog.cancel this.elements.cancel.hidden = dialog.cancel === '' this.elements.message.innerText = dialog.message /* If sounds exists, update `src` */ this.elements.soundAccept.src = dialog.soundAccept || '' this.elements.soundOpen.src = dialog.soundOpen || '' /* A target can be added (from the element invoking the dialog */ this.elements.target = dialog.target || '' /* Optional HTML for custom dialogs */ this.elements.template.innerHTML = dialog.template || '' /* Grab focusable elements */ this.focusable = this.getFocusable() this.hasFormData = this.elements.fieldset.elements.length > 0 if (dialog.soundOpen) { this.elements.soundOpen.play() } this.toggle(true) if (this.hasFormData) { /* If form elements exist, focus on that first */ this.focusable[0].focus() this.focusable[0].select() } else { this.elements.accept.focus() } }
Phew! That was a lot of code. Now we can show the <dialog> element in all browsers. But we still need to mimic the functionality that waits for a user’s input after execution, like the native alert(), confirm(), and prompt() methods. For that, we need a Promise and a new method I’m calling waitForUser():
This method returns a Promise. Within that, we add event listeners for “cancel” and “accept” that either resolve false (cancel), or true (accept). If formData exists (for custom dialogs or prompt), these will be collected with a helper method, then returned in an object:
We can remove the event listeners immediately, using { once: true }.
To keep it simple, I don’t use reject() but rather simply resolve false.
Hiding the <dialog>
Earlier on, we added event listeners for the built-in cancel event. We call this event when the user clicks the “cancel” button or presses the Escape key. The cancel event removes the open attribute on the <dialog>, thus hiding it.
Where to :focus?
In our open() method, we focus on either the first focusable form field or the “Accept” button:
if (this.hasFormData) { this.focusable[0].focus() this.focusable[0].select() } else { this.elements.accept.focus() }
But is this correct? In the W3’s “Modal Dialog” example, this is indeed the case. In Scott Ohara’s example though, the focus is on the dialog itself — which makes sense if the screen reader should read the text we defined in the aria-labelledby attribute earlier. I’m not sure which is correct or best, but if we want to use Scott’s method. we need to add a tabindex="-1" to the <dialog> in our init method:
this.dialog.tabIndex = -1
Then, in the open() method, we’ll replace the focus code with this:
this.dialog.focus()
We can check the activeElement (the element that has focus) at any given time in DevTools by clicking the “eye” icon and typing document.activeElement in the console. Try tabbing around to see it update:
Clicking the “eye” icon
Adding alert, confirm, and prompt
We’re finally ready to add alert(), confirm() and prompt() to our Dialog class. These will be small helper methods that replace JavaScript dialogs and the original syntax of those methods. All of them call the open()method we created earlier, but with a settings object that matches the way we trigger the original methods.
Let’s compare with the original syntax.
alert() is normally triggered like this:
window.alert(message);
In our Dialog, we’ll add an alert() method that’ll mimic this:
We set cancel and template to empty strings, so that — even if we had set default values earlier — these will not be hidden, and only message and accept are shown.
confirm() is normally triggered like this:
window.confirm(message);
In our version, similar to alert(), we create a custom method that shows the message, cancel and accept items:
{ target: event.target } is a reference to the DOM element that calls the method. We’ll use that to refocus on that element when we close the <dialog>, returning the user to where they were before the dialog was fired.
We ought to test this
It’s time to test and make sure everything is working as expected. Let’s create a new HTML file, import the class, and create an instance:
<script type="module"> import Dialog from './dialog.js'; const dialog = new Dialog(); </script>
Try out the following use cases one at a time!
/* alert */ dialog.alert('Please refresh your browser') /* or */ dialog.alert('Please refresh your browser').then((res) => { console.log(res) }) /* confirm */ dialog.confirm('Do you want to continue?').then((res) => { console.log(res) }) /* prompt */ dialog.prompt('The meaning of life?', 42).then((res) => { console.log(res) })
Then watch the console as you click “Accept” or “Cancel.” Try again while pressing the Escape or Enter keys instead.
Async/Await
We can also use the async/await way of doing this. We’re replacing JavaScript dialogs even more by mimicking the original syntax, but it requires the wrapping function to be async, while the code within requires the await keyword:
document.getElementById('promptButton').addEventListener('click', async (e) => { const value = await dialog.prompt('The meaning of life?', 42); console.log(value); });
Cross-browser styling
We now have a fully-functional cross-browser and screen reader-friendly HTML dialog element that replaces JavaScript dialogs! We’ve covered a lot. But the styling could use a lot of love. Let’s utilize the existing data-component and data-ref-attributes to add cross-browser styling — no need for additional classes or other attributes!
What if the standard alert(), confirm() and prompt() methods we are mimicking won’t do the trick for your specific use case? We can actually do a bit more to make the <dialog> more flexible to cover more than the content, buttons, and functionality we’ve covered so far — and it’s not much more work.
Earlier, I teased the idea of adding a sound to the dialog. Let’s do that.
You can use the template property of the settings object to inject more HTML. Here’s a custom example, invoked from a <button> with id="btnCustom" that triggers a fun little sound from an MP3 file:
Here’s a Pen with everything we built! Open the console, click the buttons, and play around with the dialogs, clicking the buttons and using the keyboard to accept and cancel.
So, what do you think? Is this a good way to replace JavaScript dialogs with the newer HTML dialog element? Or have you tried doing it another way? Let me know in the comments!
A little bit of animation on a site can add some flair, impress users, and get their attention. You could have them run, no matter where they are on the page, immediately when the page loads. But what if your website is fairly long so it took some time for the user to scroll down to that element? They might miss it.
You could have them run all the time, but perhaps the animation is best designed so that you for sure see the beginning of it. The trick is to start the animation when the user scrolls down to that element — scroll-triggered animation, if you will.
To tackle this we use scroll triggers. When the user scrolls down to any particular element, we can use that event to do something. It could be anything, even the beginning of an animation. It could even be scroll-triggered lazy loading on images or lazy loading a whole comments section. In that way, we won’t force users to download elements that aren’t in the viewport on initial page load. Many users may never scroll down at all, so we really save them (and us) bandwidth and load time.
Scroll triggers are very useful. There are many libraries out there that you can use to implement them, like Greensock’s popular ScrollTrigger plugin. But you don’t have to use a third-party library, particularly for fairly simple ideas. In fact, you can implement it yourself using only a small handful of vanilla JavaScript. That is what we are going to do in this article.
Here’s how we’ll make our scroll-triggered event
Create a function called scrollTrigger we can apply to certain elements
Apply an .active class on an element when it enters the viewport
Animate that .active class with CSS
There are times where adding a .active class is not enough. For example, we might want to execute a custom function instead. That means we should be able to pass a custom function that executes when the element is visible. Like this:
We’ll also attempt to handle scroll triggers for older non-supporting browsers.
But first, the IntersectionObserver API
The main JavaScript feature we’re going to use is the Intersection Observer. This API provides a way to asynchronously observe changes in the intersection of a target element — and it does so more in a more performant way than watching for scroll events. We will use IntersectionObserver to monitor when scrolling reaches the point where certain elements are visible on the page.
Let’s start building the scroll trigger
We want to create a function called scrollTrigger and this function should take a selector as its argument.
function scrollTrigger(selector) { // Multiple element can have same class/selector, // so we are using querySelectorAll let els = document.querySelectorAll(selector) // The above `querySelectorAll` returns a nodeList, // so we are converting it to an array els = Array.from(els) // Now we are iterating over the elements array els.forEach(el => { // `addObserver function` will attach the IntersectionObserver to the element // We will create this function next addObserver(el) }) } // Example usage scrollTrigger('.scroll-reveal')
Now let’s create the addObserver function that want to attach to the element using IntersectionObserver:
function scrollTrigger(selector){ let els = document.querySelectorAll(selector) els = Array.from(els) els.forEach(el => { addObserver(el) }) } function addObserver(el){ // We are creating a new IntersectionObserver instance let observer = new IntersectionObserver((entries, observer) => { // This takes a callback function that receives two arguments: the elements list and the observer instance. entries.forEach(entry => { // `entry.isIntersecting` will be true if the element is visible if(entry.isIntersecting) { entry.target.classList.add('active') // We are removing the observer from the element after adding the active class observer.unobserve(entry.target) } }) }) // Adding the observer to the element observer.observe(el) } // Example usage scrollTrigger('.scroll-reveal')
If we do this and scroll to an element with a .scroll-reveal class, an .active class is added to that element. But notice that the active class is added as soon as any small part of the element is visible.
But that might be overkill. Instead, we might want the .active class to be added once a bigger part of the element is visible. Well, thankfully, IntersectionObserver accepts some options for that as its second argument. Let’s apply those to our scrollTrigger function:
// Receiving options as an object // If the user doesn't pass any options, the default will be `{}` function scrollTrigger(selector, options = {}) { let els = document.querySelectorAll(selector) els = Array.from(els) els.forEach(el => { // Passing the options object to the addObserver function addObserver(el, options) }) } // Receiving options passed from the scrollTrigger function function addObserver(el, options) { let observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if(entry.isIntersecting) { entry.target.classList.add('active') observer.unobserve(entry.target) } }) }, options) // Passing the options object to the observer observer.observe(el) } // Example usage 1: // scrollTrigger('.scroll-reveal') // Example usage 2: scrollTrigger('.scroll-reveal', { rootMargin: '-200px' })
And just like that, our first two agenda items are fulfilled!
Let’s move on to the third item — adding the ability to execute a callback function when we scroll to a targeted element. Specifically, let’s pass the callback function in our options object as cb:
function scrollTrigger(selector, options = {}) { let els = document.querySelectorAll(selector) els = Array.from(els) els.forEach(el => { addObserver(el, options) }) } function addObserver(el, options){ let observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if(entry.isIntersecting){ if(options.cb) { // If we've passed a callback function, we'll call it options.cb(el) } else{ // If we haven't, we'll just add the active class entry.target.classList.add('active') } observer.unobserve(entry.target) } }) }, options) observer.observe(el) } // Example usage: scrollTrigger('.loader', { rootMargin: '-200px', cb: function(el){ el.innerText = 'Loading...' // Done loading setTimeout(() => { el.innerText = 'Task Complete!' }, 1000) } })
Great! There’s one last thing that we need to take care of: legacy browser support. Certain browsers might lack support for IntersectionObserver, so let’s handle that case in our addObserver function:
function scrollTrigger(selector, options = {}) { let els = document.querySelectorAll(selector) els = Array.from(els) els.forEach(el => { addObserver(el, options) }) } function addObserver(el, options) { // Check if `IntersectionObserver` is supported if(!('IntersectionObserver' in window)) { // Simple fallback // The animation/callback will be called immediately so // the scroll animation doesn't happen on unsupported browsers if(options.cb){ options.cb(el) } else{ entry.target.classList.add('active') } // We don't need to execute the rest of the code return } let observer = new IntersectionObserver((entries, observer) =>; { entries.forEach(entry => { if(entry.isIntersecting) { if(options.cb) { options.cb(el) } else{ entry.target.classList.add('active') } observer.unobserve(entry.target) } }) }, options) observer.observe(el) } // Example usages: scrollTrigger('.intro-text') scrollTrigger('.scroll-reveal', { rootMargin: '-200px', }) scrollTrigger('.loader', { rootMargin: '-200px', cb: function(el){ el.innerText = 'Loading...' setTimeout(() => { el.innerText = 'Task Complete!' }, 1000) } })
Here’s that live demo again:
And that’s all for this little journey! I hope you enjoyed it and learned something new in the process.
What if a backdoor literally cannot be seen and thus evades detection even from thorough code reviews?
I’ll post the screenshot of the exploit from the post with the actual exploit circled:
If you were really looking super closely you’d probably see that, but I can see how it would be easy to miss as it would avoid any linting problems and doesn’t mess up syntax highlighting at all. Then the way this code is written, the commands are executed:
Each element in the array, the hardcoded commands as well as the user-supplied parameter, is then passed to the exec function. This function executes OS commands.
They consider it worthy of change:
The Cambridge team proposes restricting Bidi Unicode characters. As we have shown, homoglyph attacks and invisible characters can pose a threat as well.
It doesn’t have any error handling or anything, but hey, it works:
Now imagine how some websites give you a URL to JavaScript in order to do stuff. CodePen does this for our Embedded Pens feature.
That URL is:
https://cpwebassets.codepen.io/assets/embed/ei.js
I can proxy that URL just as easily:
Doing nothing special, it even serves up the right content-type header and everything:
Cloudflare Workers gives you a URL for them, which is decently nice, but you can also very easily “Add a Route” to a worker on your own website. So, here I’ll make a URL on CSS-Tricks to serve up that Worker. Lookie lookie, it does just what it says it’s going to do:
CSS-Tricks.com serving up a JavaScript file that is actually just proxied from CodePen. I’m probably not going to leave this URL live, it’s just a demo.
Right from css-tricks.com and it’ll load that JavaScript. It will look to the browser like first-party JavaScript, but it will really be proxied third-party JavaScript.
Why? Well nobody is going to block your first-party JavaScript. If you were a bit slimy, you could run all your scripts for ads this way to avoid ad blockers. I have mixed feelings there. I feel like if you wanna block ads you should be able to block ads without having to track down specific scripts on specific sites to do that. On the other hand, proxying some third-party resources sometimes seems kinda fine? Like if it’s your own site and you’re just trying to get around some CORS issue… that would be fine.
More in the middle is something like analytics. I recently blogged “Comparing Google Analytics and Plausible Numbers” where I discussed Plausible, a third-party analytics service that “is built for privacy-conscious site owners.” So, ya know, theoretically trustable and not third-party JavaScript that is terribly worrisome. But still, it doesn’t do anything to really help site visitors and is in the broad category of analytics, so I could see it making its way onto blocklists, thus giving you less accurate information over time as more and more people block it.
The default usage for Plausible is third-party JavaScript
But as we talked about, very few people are going to block first-party JavaScript, so proxying would theoretically deliver more accurate information. In fact, they have docs for proxying. It’s slightly more involved, and it’s over my head as to exactly why, but hey, it works.
I’ve done this proxying as a test. So now I have data from just using the third-party JavaScript directly (from the last article):
Metric
Plausible (No Proxy)
Google Analytics
Unique Visitors
973k
841k
Pageviews
1.4m
1.5m
Bounce Rate
82%
82%
Visit Duration
1m 31s
1m 24s
Data from one week of non-proxied third-party JavaScript integration
And can compare it to an identical-in-length time period using the proxy:
Metric
Plausible (Proxy)
Google Analytics
Unique Visitors
1.32m
895k
Pageviews
2.03m
1.7m
Bounce Rate
81%
82%
Visit Duration
1m 35s
1m 24s
Data from one week of proxied third-party JavaScript integration
So the proxy really does highly suggest that doing it that way is far less “blocked” than even out-of-the-box Plausible is. The week tested was 6%¹ busier according to the unchanged Google Analytics. I would have expected to see 15.7% more Unique Visitors that week based on what happened with the non-proxied setup (meaning 1.16m), but instead I saw 1.32m, so the proxy demonstrates a solid 13.8% increase in seeing unique visitors versus a non-proxy setup. And comparing the proxied Plausible setup to Google Analytics directly shows a pretty staggering 32% more unique visitors.
With the non-proxied setup, I actually saw a decrease in pageviews (-6.6%) on Plausible compared to Google Analytics, but with the proxied setup I’m seeing 19.4% more pageviews. So the numbers are pretty wishy-washy but, for this website, suggest something in the ballpark of 20-30% of users blocking Google Analytics.
I always find it so confusing to figure out the percentage increase between two numbers. The trick that ultimately works for my brain is (final - initial) / final * 100.
High five to Jeremy on the big release of Responsible JavaScript on A Book Apart. There is a lot of talk about how the proliferation of JavaScript has had a negative impact on the web, but now we have the canonical reference tome.
The book is just chock-full of Jeremey framing some of the biggest arguments discussions about modern web development, dissecting them, and helping us learn from them. I say “modern web development” there on purpose, because JavaScript has gotten to be such a massive part of building websites these days that the two terms are almost synonymous, for better or worse. While the book title is Responsible JavaScript, it might as well be “Responsible Web Development” to make it go hand and hand with Scott’s book (and Mat’s book, if you need a more gentle introduction to JavaScript).
I like how Jermey blends the old and new together. Readers are introduced and shown how techniques as old as the web (like progressive enhancement) are still useful today and perhaps even more useful than they have ever been. But this isn’t a history novel. New technology (like service workers) is handled with equal grace, and modern techniques for performance gains are given the credit they are due (like build tools and code splitting).
As an aside here — have you ever had an inkling to write a tech book? I have both heard and given this advice: Write a blog post first, or maybe a whole bunch of blog posts. That’ll prove you clearly have words to say about this. Plus it will get the energy of your idea into the world. You might get feedback and constructive insights on the idea as it is shared. Then, hopefully, you can turn that blog post into a talk. That’ll really get you thinking deeply about your idea while getting even more comfortable with the idea of sharing it clearly. And, if all goes well, turn all of that into a book!
Let’s see what happened here. Jeremy wrote a couple of blog posts (hey, nice title). They became a talk (hey, nice title). And that turned into a book (hey, nice title).
If you have a page that includes a lot of information, it’s a good idea to let users search for what they might be looking for. I’m not talking about searching a database or even searching JSON data — I’m talking about literally searching text on a single rendered web page. Users can already use the built-in browser search for this, but we can augment that by offering our own search functionality that filters down the page making matching results easier to find and read.
Well, you might know JavaScript already. JavaScript is going to handle all the interactivity in this journey. It’s going to…
find all the content we want to search through,
watch what a user types in the search input,
filter the innerText of the searchable elements,
test if the text includes the search term (.includes() is the heavy lifter here!), and
toggle the visibility of the (parent) elements, depending on if they include the search term or not.
Alright, we have our requirements! Let’s start working.
The basic markup
Let’s assume we have a FAQ page. Each question is a “card” which has a title and content:
<h1>FAQ Section</h1> <div class="cards"> <h3>Who are we</h3> <p>It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularized </p> </div> <div class="cards"> <h3>What we do</h3> <p>It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularized </p> </div> <div class="cards"> <h3>Why work here</h3> <p>It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularized</p> </div> <div class="cards"> <h3>Learn more</h3> <p>Want to learn more about us?</p> </div>
Imagine there are a lot of questions on this page.
To get ready for the interactivity, we’ll have this one line of CSS. This gives us a class we can add/remove depending on the search situation when we get to the JavaScript:
.is-hidden { display: none; }
Let’s add a search input with an event that fires when it is interacted with:
And here’s the JavaScript that does everything else!
function liveSearch() { // Locate the card elements let cards = document.querySelectorAll('.cards') // Locate the search input let search_query = document.getElementById("searchbox").value; // Loop through the cards for (var i = 0; i < cards.length; i++) { // If the text is within the card... if(cards[i].innerText.toLowerCase() // ...and the text matches the search query... .includes(search_query.toLowerCase())) { // ...remove the `.is-hidden` class. cards[i].classList.remove("is-hidden"); } else { // Otherwise, add the class. cards[i].classList.add("is-hidden"); } } }
You can probably go line-by-line there and reason out what it is doing. It finds all the cards and the input and saves references to them. When a search event fires, it loops through all the cards, determines if the text is within the card or not. It the text in the card matches the search query, the .is-hidden class is removed to show the card; if not, the class is there and the card remains hidden.
To make sure our JavaScript doesn’t run too much (meaning it would slow down the page), we will run our liveSearch function only after waiting an “X” number of seconds.
<!-- Delete on Input event on this input --> <label for="searchbox">Search</label> <input type="search" id="searchbox">
// A little delay let typingTimer; let typeInterval = 500; // Half a second let searchInput = document.getElementById('searchbox'); searchInput.addEventListener('keyup', () => { clearTimeout(typingTimer); typingTimer = setTimeout(liveSearch, typeInterval); });
What about fuzzy searches?
Let’s say you want to search by text that is not visible to user. The idea is sort of like a fuzzy search, where related keywords return the same result as an exact match. This helps expand the number of cards that might “match” a search query.
There are two ways to do this. The first is using a hidden element, like a span, that contains keywords:
<div class="cards"> <h3>Who are we</h3> <p>It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularized</p> <!-- Put any keywords here --> <span class="is-hidden">secret</span> </div>
for (var i = 0; i < cards.length; i++) { if(cards[i].textContent.toLowerCase() .includes(search_query.toLowerCase())) { cards[i].classList.remove("is-hidden"); } else { cards[i].classList.add("is-hidden"); } }
Try typing “secret” on a search box, it should reveal this card, even though “secret” isn’t a displayed on the page.
A second approach is searching through an attribute. Let’s say we have a gallery of images. We can put the keywords directly on the alt attribute of the image. Try typing “kitten” or “human” in the next demo. Those queries are matching what’s contained in the image alt text.
For this to work, we need to change innerText to getAttribute('alt') since we want to look through alt attributes in addition to what’s actually visible on the page.
for (var i = 0; i < cards.length; i++) { if(cards[i].getAttribute('alt').toLowerCase() .includes(search_query.toLowerCase())) { cards[i].classList.remove("is-hidden"); } else { cards[i].classList.add("is-hidden"); } }
Depending on your needs, you could put your keywords in another attribute, or perhaps a custom one.
Caveat
Again, this isn’t a search technology that works by querying a database or other data source. It works only if you have all the searchable content in the DOM on that page, already rendered.
So, yeah, there’s that. Just something to keep in mind.
Wrapping up
Obviously, I really like this technique, enough to use it on a production site. But how else might you use something like this? An FAQ page is a clear candidate, as we saw, but any situation that calls for filtering any sort of content is fit for this sort of thing. Even a gallery of images could work, using the hidden input trick to search through the alt tag content of the images.
Whatever the case, I hope you find this helpful. I was surprised that we can get a decently robust search solution with a few lines of vanilla JavaScript.
Have you used this technique before, or something like it? What was your use case?
Scroll shadows are when you can see a little inset shadow on elements if (and only if) you can scroll in that direction. It’s just good UX. You can actually pull it off in CSS, which I think is amazing and one of the great CSS tricks. Except… it just doesn’t work on iOS Safari. It used to work, and then it broke in iOS 13, along with some other useful CSS things, with no explanation why and has never been fixed.
So, now, if you really want scroll shadows (I think they are extra useful on mobile browsers anyway), it’s probably best to reach for JavaScript.
I’m bringing this up now because I see Jonnie Hallman is blogging about tit again. He mentioned it as an awesome little detail back in May. There are certain interfaces where scroll shadows really extra make sense.
Taking a step back, I thought about the solution that currently worked, using scroll events. If the scroll area has scrolled, show the top and left shadows. If the scroll area isn’t all the way scrolled, show the bottom and right shadows. With this in mind, I tried the simplest, most straight-forward, and least clever approach by putting empty divs at the top, right, bottom, and left of the scroll areas. I called these “edges”, and I observed them using the Intersection Observer API. If any of the edges were not intersecting with the scroll area, I could assume that the edge in question had been scrolled, and I could show the shadow for that edge. Then, once the edge is intersecting, I could assume that the scroll area has reached the edge of the scroll, so I could hide that shadow.
Clever clever. No live demo, unfortunately, but read the post for a few extra details on the implementation.
Other JavaScript-powered examples
I do think if you’re going to do this you should go the IntersectionObserver route though. Would love to see someone port the best of these ideas all together (wink wink).