Tag: Comparing

Comparing JAWS, NVDA, and VoiceOver

A screen reader is an important accessibility tool for people with no or limited vision. People who are blind or those with low vision can use a screen reader to navigate the computer. Screen readers will read contents on the screen and explain to the user what is on the page. Screen readers allow people to use the computer for daily tasks.

There are many screen reader software available for people through their operating system or through open source projects.

A 2021 research by WebAim found that from 1568 responders, more than 53.7 percent of people surveyed used JAWS on Windows, more than 30.7 percent of people used NVDA on Windows and little over 6.5 percent of people used VoiceOver on macOS.

JAWS and NVDA for Windows and VoiceOver for macOS are the most popular screen readers people use.

First, I should clarify that this article will be written from my point of view. To give background, I have been a front-end developer at a non-profit for people with learning differences for over three years. I, along with my colleagues, seek to make our projects more accessible every day. I am not visually impaired and do not use these tools on a regular basis. For work, I have a Mac machine and test accessibility using VoiceOver.

Here is my planned testing methodology:

  1. Navigate the page by heading, until “Accessibility APIs” section.
  2. In the “Accessibility APIs” section, read the content and the unordered list inside.
  3. TAB to hear focusable items in the unordered list.
  4. Jump to the Search field.
  5. TAB to hear a few items in the navigation section

To find similarities and differences between them, I decided to test a set of steps with each screen reader on a Wikipedia page about screen readers. I will browse the web with Chrome for my tests. Testing all screen readers on the same page and browser will reduce the amount of variables and keep the tests consistent.


JAWS is an acronym for Job Access With Speech and is the most widely used screen reader in the world. It is only available on Windows. Depending on the plan and features, JAWS can be purchased anywhere from $ 90 yearly license all the way to $ 1605 for perpetual license.

JAWS has predefined keyboard commands to navigate the web. Full list of keyboard commands can be found on their website.



In the beginning of the demo, I am clicking on H key on my keyboard to go to the next heading. JAWS is moving down the page, reading me the headings along with their level.

Later in the video, I am clicking on number 2 and number 3 on my keyboard to have JAWS read Heading Levels 2s then later Heading Levels 3s. This is a great feature because we can move down the page and sections by heading level and get a better sense of the page layout.

When I reach the “Accessible APIs” section, I press the DOWN ARROW key until the third item in the unordered list.

Later in the demo, I am clicking on the TAB key for JAWS to read to me the next focusable item on the page, which is inside this list. I click TAB until I reach a focusable element in another section.

Then I press F key to focus on the search field, which JAWS reads to me.

Then I click on TAB and JAWS focuses on the navigation elements that are on the side of the page.

Pros & Cons


  • JAWS is more customizable than other screen readers.
  • There are more options to navigate through the page.
  • JAWS is industry standard.
  • Widely used, which means there are lots of user to user support.


  • JAWS is more complicated to use than NVDA or VoiceOver.
  • Some commands are not intuitive.
  • There are a lot more commands for the user to learn.
  • More learning curve for users.
  • JAWS is also not available on the Mac, which limits its users.
  • Costs anywhere between $ 90 – $ 1605 for the user.
  • JAWS has different key commands for desktop and laptop which may make it harder for users to transfer knowledge and may cause confusion.


NVDA, or NonVisual Digital Access, is available on Windows only. Users need to download the software from NVDA’s website, NVAccess. This software is free to download but does not come already installed on Windows machines. NVDA is the second most popular screen reader in the world according to WebAim’s 2021 survey.

Like other screen readers, NVDA has defined keyboard commands to navigate the web. NVDA’s full keyboard commands can be found on their website.



In the demo I am clicking on H key on the keyboard to go to the next heading. First, NVDA reads me Heading Level 1, which is “Screen reader”. Then NVDA goes to read Heading Level 2s and 3s.

When I reach “References” I begin to click on TAB on my keyboard for NVDA to focus on next focusable items.

After focusing on a few items on the list, I click ENTER and go to the New York Times page.

Pros & Cons


  • Overall, I found NVDA was able to provide me with information on the screen.
  • The out-of-the-box keyboard commands were easy to use and easy to learn.
  • NVDA is open source, which means the community can update and fix.
  • NVDA is free, which makes it an affordable option to Windows users.


  • NVDA is not available on the Mac, which limits its users.


VoiceOver is the screen reader used in Mac. VoiceOver is only available on Mac not available in Windows. VoiceOver is free and is already installed on the computer, which removes barriers because this is part of the computer setup and the user does not have to download or purchase any additional software.

VoiceOver has defined keyboard commands to navigate the web. VoiceOver’s full keyboard commands can be found on their website.


VoiceOver Demo

In the demo, I am on a Wikipedia page and I am clicking on the VoiceOver Command (which is Control+Option) along with Command+H to navigate through the headings. VoiceOver reads the headings in order, starting from Heading Level 1, “Screen Reader”, to Heading Level 2, “Contents”, to Heading Level 3, and so on.

When I reach the “Accessibility APIs” section, I click on VoiceOver Command plus the RIGHT ARROW, to tell VoiceOver that I want it to read this section. Later I am clicking on the VoiceOver Command plus the RIGHT ARROW on my keyboard, to navigate the section.

When I get on to the third item on the unordered list, I press TAB on my keyboard to focus on the next focusable element.

I press TAB a few times, then I press VoiceOver Command plus U, to open the Form Control Menu. In the menu, I press DOWN ARROW until I hear the “Search Wikipedia” option. When I hear it, I click ENTER and the screen reader focuses on the form field. In the form field, I press TAB to navigate to the navigation section.

Pros & Cons


  • VoiceOver is easy to use and learn.
  • VoiceOver’s commands are intuitive.
  • Free tool that comes installed in every macOS device.


  • VoiceOver is also not available on Windows, which limits its users.
  • VoiceOver is not an app and can only be updated when Apple releases macOS update.

Key Takeaways

A screen reader is an important accessibility tool for people with no or limited vision. Screen readers allow people to use the computer for daily tasks.

There are many screen reader softwares available. In this article I compared JAWS, NVDA, and VoiceOver.

Here is a comparison chart overview of the three screen readers:

Operating System Windows Windows macOS
Price $ 90 – $ 1695 Free Free
# of users 30% 50% 6%
Ease of Use Hard Easy Easy

I found that for basic screen reader testing, most screen readers follow a similar keystroke pattern and knowledge from one screen reader can be used for others.

All screen readers have their pros and cons. Ultimately, it’s up to user preference and also the operating system they use to determine which screen reader software is best for them.

Previously: “Small Tweaks That Can Make a Huge Impact on Your Website’s Accessibility” (2018), and “Why, How, and When to Use Semantic HTML and ARIA” (2019), “15 Things to Improve Your Website Accessibility” (2020), “5 Accessibility Quick Wins You Can Implement Today” (2022).

Comparing JAWS, NVDA, and VoiceOver originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.


, , ,

Comparing Node JavaScript to JavaScript in the Browser

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.

Screenshot of a quiz editor written in Node JavaScript that contains four inputs two checkboxes and four buttons.

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:


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:

  1. We declare the result variable with let result;. We haven’t assigned anything to this variable so its value is undefined.
  2. 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.
  3. Our function returns result. As our callback hasn’t run yet, result still holds a value of undefined.
  4. 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.


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:

get({ table: "answer", column: "answerid", value: 2 })   .then((answer) => {     return get({       table: "question",       column: "questionid",       value: answer.answerquestion,     });   })   .then((question) => {     return get({       table: "quiz",       column: "quizid",       value: question.questionquiz,     });   })   .then((quiz) => {     console.log(quiz);   })   .catch((error) => {     console.log(error);   } );

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.


With async/await we can write our asynchronouse code almost the same way we write synchronous code. Sarah Drasner has a great post explaining async/await.

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:

async function printQuizFromAnswer() {   const answer = await get({ table: "answer", column: "answerid", value: 2 });   const question = await get({     table: "question",     column: "questionid",     value: answer.answerquestion,   });   const quiz = await get({     table: "quiz",     column: "quizid",     value: question.questionquiz,   });   console.log(quiz); }  printQuizFromAnswer();

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.


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:

And if you are planning to hold data in an SQL database, read up on SQL Basics.

Comparing Node JavaScript to JavaScript in the Browser originally published on CSS-Tricks. You should get the newsletter.


, , ,

Introducing Svelte, and Comparing Svelte with React and Vue

Josh Collingsworth is clearly a big fan of Svelte, so while this is a fun and useful comparison article, it’s here to crown Svelte the winner all the way through.

A few things I find compelling:

One of the things I like most about Svelte is its HTML-first philosophy. With few exceptions, Svelte code is entirely browser-readable HTML and JavaScript. In fact, technically, you could call Svelte code as a small superset of HTML.


Svelte is reactive by default. This means when a variable is reassigned, every place it’s used or referenced also updates automatically. (React and Vue both require you to explicitly initialize reactive variables.)

I do find the component format nice to look at, like how you just write HTML. You don’t even need a <template> around it, or to return anything. I imagine Astro took inspiration from this in how you can also just chuck a <style> tag in there and scope styles if you want. But I think I prefer how the “fenced” JavaScript at the top only runs during the build by default.

P.S. I really like Josh’s header/footer random square motif so I tried to reverse engineer it:

To Shared LinkPermalink on CSS-Tricks

The post Introducing Svelte, and Comparing Svelte with React and Vue appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , ,

Comparing Google Analytics and Plausible Numbers

I saw this blog post the other day: 58% of Hacker News, Reddit and tech-savvy audiences block Google Analytics. That’s an enticing title to me. I’ve had Google Analytics on this site literally from the day I launched it. While we tend to see some small growth each year, I’m also aware that the ad-blocking usage (and general third-party script blocking) goes up over time. So maybe we have more growth than we can easily visualize in Google Analytics?

The level of Google Analytics blockage varies by industry, audience, the device used and the individual website. In a previous study, I’ve found that less than 10% of visitors block Google Analytics on foodie and lifestyle sites but more than 25% block it on tech-focused sites.

Marko Saric

Marko looked at three days of data on his own website when he had a post trending on both Hacker News and Reddit, and that’s where the 58% came from. Plausible analytics reported 50.9k unique visitors and Google Analytics reported 21.1k. (Google Analytics calls them “Users.”)

I had to try this myself. If we’re literally getting double the traffic Google Analytics says we are, I’d really like to know that. So for the last week, I’ve had Plausible installed on this site (they have a generous unlimited 30-day free trial).

Here’s the Plausible data:

And here’s the Google Analytics data for the same exact period:

It’ll be easier to digest in a table:

Metric Plausible Google Analytics
Unique Visitors 973k 841k
Pageviews 1.4m 1.5m
Bounce Rate 82% 82%
Visit Duration 1m 31s 1m 24s

So… the picture isn’t exactly clear. On one hand, Plausible reports 15% more unique visitors than Google Analytics. Definitely not double, but more! But then, as far as raw traffic is concerned (which actually matters more to me as a site that has ads), Plausible reports 5% less traffic. So it’s still fairly unclear to me what the real story is.

I do think Plausible is a pretty nice bit of software. Easy to install. Simple and nice charts. Very affordable.

Plausible is also third-party JavaScript

I’m sure less people block Plausible’s JavaScript, as, basically, it’s less-known and not Google. But it’s still third-party JavaScript and just as easily blockable as anything else. It’s not a fundamentally different way to measure traffic.

What is fundamentally different is something like Netlify Analytics, where the data comes from server logs that are not blockable (we’ve mentioned this before). That has its own issues, like the fact that Netlify counts bots and Google Analytics doesn’t. Maybe the best bet is something like server-side Google Analytics? That’s just way more technical debt than I’m ready for. Certainly a lot harder than slapping a script tag on the page.

Multiple sources

It felt weird to have multiple analytics tracking on the site at the same time. If I was doing things like tracking custom events, that would get even weirder. If I was going to continue to do it all client-side, I’d probably reach for something like Analytics which is designed to abstract that. But I prefer the idea of sending the data from the client once, and then if it has to go multiple places, do that server-side. That’s basically the entire premise of Segment, which is expensive, but you can self-host or probably write your own without terribly too much trouble. Just seems smart to me to keep less data going over the wire, and having it be first-party JavaScript.

The post Comparing Google Analytics and Plausible Numbers appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , ,

Comparing HTML Preprocessor Features

(This is a sponsored post.)

Of the languages that browsers speak, I’d wager that the very first one that developers decided needed some additional processing was HTML. Every single CMS in the world (aside from intentionally headless-only CMSs) is essentially an elaborate HTML processor: they take content and squoosh it together with HTML templates. There are dozens of other dedicated HTML processing languages that exist today.

The main needs of HTML processing being:

  • Compose complete HTML documents from parts
  • Template the HTML by injecting variable data

There are plenty of other features they can have, and we’ll get to that, but I think those are the biggies.

This research is brought to you by support from Frontend Masters, CSS-Tricks’ official learning partner.

Need front-end development training?

Frontend Masters is the best place to get it. They have courses on all the most important front-end technologies, from React to CSS, from Vue to D3, and beyond with Node.js and Full Stack.

Diagram showing partials and {{ data }} turning into a complete HTML document.

Consider PHP. It’s literally a “Hypertext Preprocessor.” On this very website, I make use of PHP in order to piece together bits of templated HTML to build the pages and complete content you’re looking at now.

<h2>   <?php the_title(); // Templating! ?> </h2>  <?php include("metadata.php"); // Partials! ?>

In the above code, I’ve squooshed some content into an HTML template, which calls another PHP file that likely contains more templated HTML. PHP covers the two biggies for HTML processing and is available with cost-friendly hosting — I’d guess that’s a big reason why PHP-powered websites power a huge chunk of the entire internet.

But PHP certainly isn’t the only HTML preprocessor around, and it requires a server to work. There are many others, some designed specifically to run during a build process before the website is ever requested by users.

Let’s go language-by-language and look at whether or not it supports certain features and how. When possible the link of the preprocessor name links to relevant docs.

Does it allow for templating?

Can you mix in data into the final HTML output?

Processor Example
- var title = "On Dogs: Man's Best Friend";
- var author = "enlore";
h1= title
p Written with love by #Chris Coyier
<%= title %>
<%= description %>

<%= @logged_in_user.name %>

<?php echo $ post.title; ?>
<?php echo get_post_description($ post.id) ?>
Also has HEREDOC syntax.
td.name = item.name
<h1><%= post.title %></h1>
<div class="subhead"><%= post.subtitle %></div>
Hello {{ user.name }}!
Go html/template
{{ .Title }}
{{ $ address }}
{{firstname}} {{lastname}}
Hello {{ firstname }}!
{{ foo.bar }}
<h1>{{ title }}</h1>
<!-- $ myVar = We finish each other's sandwiches. -->
<p> <!-- $ myVar --> </p>

Does it do partials/includes?

Can you compose HTML from smaller parts?

Processor Example
include includes/head.pug
<%= render 'includes/head' %>
<?php include 'head.php'; ?>
<?php include_once 'meta.php'; ?>
Slim ⚠️
If you have access to the Ruby code, it looks like it can do it, but you have to write custom helpers.
=render 'meeting_info'
Liquid {% render head.html %}
{% render meta.liquid %}
Go html/template {{ partial "includes/head.html" . }}
Handlebars ⚠️
Only through registering a partial ahead of time.
Mustache {{> next_more}}
Twig {{ include('page.html', sandboxed = true) }}
Nunjucks {% include "missing.html" ignore missing %}
{% import "forms.html" as forms %}
{{ forms.label('Username') }}
Kit <!-- @import "someFile.kit" -->
<!-- @import "file.html" -->
Sergey <sergey-import src="header" />

Does it do local variables with includes?

As in, can you pass data to the include/partial for it to use specifically? For example, in Liquid, you can pass a second parameter of variables for the partial to use. But in PHP or Twig, there is no such ability—they can only access global variables.

Processor Example
ERB <%= render(
partial: "card",
locals: {
title: "Title"
) %>
Haml .content
= render :partial => 'meeting_info', :locals => { :info => @info }
Liquid {% render "name", my_variable: my_variable, my_other_variable: "oranges" %}
Go html/template {{ partial "header/site-header.html" . }}
(The period at the end is “variable scoping.”)
Handlebars {{> myPartial parameter=favoriteNumber }}
Nunjucks {% macro field(name, value='', type='text') %}
<div class="field">
<input type="{{ type }}" name="{{ name }}" value="{{ value | escape }}" />
{% endmacro %}

Does it do loops?

Sometimes you just need 100 <div>s, ya know? Or more likely, you need to loop over an array of data and output HTML for each entry. There are lots of different types of loops, but having at least one is nice and you can generally make it work for whatever you need to loop.

Processor Example
PHP for ($ i = 1; $ i <= 10; $ i++) {
echo $ i;
ERB <% for i in 0..9 do %>
<%= @users[i].name %>
<% end %>
Pug for (var x = 1; x < 16; x++)
div= x
Slim - for i in (1..15)
div #{i}
Haml (1..16).each do |i|
%div #{i}
Liquid {% for i in (1..5) %}
{% endfor %}
Go html/template {{ range $ i, $ sequence := (seq 5) }}
{{ $ i }}: {{ $ sequence }
{{ end }}
Handlebars {{#each myArray}}
<div class="row"></div>
Mustache {{#myArray}}
Twig {% for i in 0..10 %}
{{ i }}
{% endfor %}
Nunjucks {% set points = [0, 1, 2, 3, 4] %}
{% for x in points %}
Point: {{ x }}
{% endfor %}

Does it have logic?

Mustache is famous for philosophically being “logic-less”. So sometimes it’s desirable to have a templating language that doesn’t mix in any other functionality, forcing you to deal with your business logic in another layer. Sometimes, a little logic is just what you need in a template. And actually, even Mustache has some basic logic.

Processor Example
Pug #user
if user.description
h2.green Description
else if authorised
h2.blue Description
ERB <% if show %>
<% endif %>
PHP <?php if (value > 10) { ?>
<?php } ?>
Slim - unless items.empty?If you turn on logic less mode:
- article
h1 = title
-! article
p Sorry, article not found
Haml if data == true
%p true
%p false
Liquid {% if user %}
Hello {{ user.name }}!
{% endif %}
Go html/template {{ if isset .Params "title" }}
<h4>{{ index .Params "title" }}</h4>
{{ end }}
Handlebars {{#if author}}
{{firstName}} {{lastName}}
It’s kind of ironic that Mustache calls itself “Logic-less templates”, but they do kinda have logic in the form of “inverted sections.”
No repos :(
Twig {% if online == false %}
Our website is in maintenance mode.
{% endif %}
Nunjucks {% if hungry %}
I am hungry
{% elif tired %}
I am tired
{% else %}
I am good!
{% endif %}
It can output a variable if it exists, which it calls “optionals”:
<dd class='<!-- $ myVar? -->'> Page 1 </dd>

Does it have filters?

What I mean by filter here is a way to output content, but change it on the way out. For example, escape special characters or capitalize text.

Processor Example
Pug ⚠️
Pug thinks of filters as ways to use other languages within Pug, and doesn’t ship with any out of the box.
Whatever Ruby has, like:
"hello James!".upcase #=> "HELLO JAMES!"
PHP $ str = "Mary Had A Little Lamb";
$ str = strtoupper($ str);
echo $ str; // Prints MARY HAD A LITTLE LAMB
Slim ⚠️
Private only?
Haml ⚠️
Very specific one for whitespace removal. Mostly for embedding other languages?
Lots of them, and you can use multiple.
{{ "adam!" | capitalize | prepend: "Hello " }}
Go html/template ⚠️
Has a bunch of functions, many of which are filter-like.
Handlebars ⚠️
Triple-brackets do HTML escaping, but otherwise, you’d have to register your own block helpers.
Twig {% autoescape "html" %}
{{ var }}
{{ var|raw }} {# var won't be escaped #}
{{ var|escape }} {# var won't be doubled-escaped #}
{% endautoescape %}
Nunjucks {% filter replace("force", "forth") %}
may the force be with you
{% endfilter %}

Does it have math?

Sometimes math is baked right into the language. Some of these languages are built on top of other languages, and thus use that other language to do the math. Like Pug is written in JavaScript, so you can write JavaScript in Pug, which can do math.

Processor Support
PHP <?php echo 1 + 1; ?>
ERB <%= 1 + 1 %>
Pug - const x = 1 + 1
p= x
Slim - x = 1 + 1
p= x
Haml %p= 1 + 1
Liquid {{ 1 | plus: 1 }}
Go html/template
{{add 1 2}}
Twig {{ 1 + 1 }}
Nunjucks {{ 1 + 1 }}

Does it have slots / blocks?

The concept of a slot is a template that has special areas within it that are filled with content should it be available. It’s conceptually similar to partials, but almost in reverse. Like you could think of a template with partials as the template calling those partials to compose a page, and you almost think of slots like a bit of data calling a template to turn itself into a complete page. Vue is famous for having slots, a concept that made its way to web components.

Processor Example
You can pull it off with “mixins”
Go html/template
Twig {% block footer %}
© Copyright 2011 by you.
{% endblock %}
Nunjucks {% block item %}
The name of the item is: {{ item.name }}
{% endblock %}
Sergey <sergey-slot />

Does it have a special HTML syntax?

HTML has <angle> <brackets> and while whitespace matters a little (a space is a space, but 80 spaces is also… a space), it’s not really a whitespace dependant language like Pug or Python. Changing these things up is a language choice. If all the language does is add in extra syntax, but otherwise, you write HTML as normal HTML, I’m considering that not a special syntax. If the language changes how you write normal HTML, that’s special syntax.

Processor Example
ERB In Ruby, if you want that you generally do Haml.
This is pretty much the whole point of Markdown.
# Title
Paragraph with [link](#link).

- List
- List

> Quote

Go html/template
Kit ⚠️
HTML comment directives.
Sergey ⚠️
Some invented HTML tags.

Wait wait — what about stuff like React and Vue?

I’d agree that those technologies are component-based and used to do templating and often craft complete pages. They also can do many/most of the features listed here. Them, and the many other JavaScript-based-frameworks like them, are also generally capable of running on a server or during a build step and producing HTML, even if it sometimes feels like an afterthought (but not always). They also have other features that can be extremely compelling, like scoped/encapsulated styles, which requires cooperation between the HTML and CSS, which is an enticing feature.

I didn’t include them because they are generally intentionally used to essentially craft the DOM. They are focused on things like data retrieval and manipulation, state management, interactivity, and such. They aren’t really focused on just being an HTML processor. If you’re using a JavaScript framework, you probably don’t need a dedicated HTML processor, although it absolutely can be done. For example, mixing Markdown and JSX or mixing Vue templates and Pug.

I didn’t even put native web components on the list here because they are very JavaScript-focused.

Other considerations

  • SpeedHow fast does it process? Do you care?
  • Language — What was in what is it written in? Is it compatible with the machines you need to support?
  • Server or Build – Does it require a web server running to work? Or can it be run once during a build process? Or both?


Templating Includes Local Variables Loops Logic Filters Math Slots Special Syntax
ERB ⚠️
Pug ⚠️
Slim ⚠️ ⚠️
Haml ⚠️
Go html/template ⚠️
Handlebars ⚠️
Kit ⚠️
Sergey ⚠️

The post Comparing HTML Preprocessor Features appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , ,

Comparing Methods for Appending and Inserting With JavaScript

Let’s say we want to add something to a webpage after the initial load. JavaScript gives us a variety of tools. Perhaps you’ve used some of them, like append, appendChild, insertAdjacentHTML, or innerHTML.

The difficult thing about appending and inserting things with JavaScript isn’t so much about the tools it offers, but which one to use, when to use them, and understanding how each one works.

Let’s try to clear things up.

Super quick context

It might be helpful to discuss a little background before jumping in. At the simplest level, a website is an HTML file downloaded from a server to a browser.

Your browser converts the HTML tags inside your HTML file into a bunch of objects that can be manipulated with JavaScript. These objects construct a Document Object Model (DOM) tree. This tree is a series of objects that are structured as parent-child relationships.

In DOM parlance, these objects are called nodes, or more specifically, HTML elements.

<!-- I'm the parent element --> <div>   <!-- I'm a child element -->   <span>Hello</span> </div>

In this example, the HTML span element is the child of the div element, which is the parent.

And I know that some of these terms are weird and possibly confusing. We say “node”, but other times we may say “element” or “object” instead. And, in some cases, they refer to the same thing, just depending on how specific we want to be .

For example, an “element” is a specific type of “node”, just like an apple is a specific type of fruit.

We can organize these terms from most general, to most specific: ObjectNodeElementHTML Element

Understanding these DOM items is important, as we’ll interact with them to add and append things with JavaScript after an initial page load. In fact, let’s start working on that.


These append and insert methods mostly follow this pattern:


Again, an element is merely an object in the DOM Tree that represents some HTML. Earlier, we had mentioned that the purpose of the DOM tree is to give us a convenient way to interact with HTML using JavaScript.

So, how do we use JavaScript to grab an HTML element?

Querying the DOM

Let’s say we have the following tiny bit of HTML:

<div id="example" class="group">   Hello World </div>

There are a few common ways to query the DOM:

// Query a specific selector (could be class, ID, element type, or attribute): const my_element1 = document.querySelector('#example')  // Query an element by its ID: const my_element2 = document.getElementbyId('example')  // Query an element by its class: const my_element3 = document.getElementbyClass('group')[0] 

In this example, all three lines query the same thing, but look for it in different ways. One looks at any of the item’s CSS selectors; one looks at the item’s ID; and one looks at the item’s class.

Note that the getElementbyClass method returns an array. That’s because it’s capable of matching multiple elements in the DOM and storing those matches in an array makes sure all of them are accounted for.

What we can append and insert

// Append Something const my_element1 = document.querySelector('#example') my_element1.append(something)

In this example, something is a parameter that represents stuff we want to tack on to the end of (i.e. append to) the matched element.

We can’t just append any old thing to any old object. The append method only allows us to append either a node or plain text to an element in the DOM. But some other methods can append HTML to DOM elements as well.

  1. Nodes are either created with document.createElement() in JavaScript, or they are selected with one of the query methods we looked at in the last section.
  2. Plain text is, well, text. It’s plain text in that it does not carry any HTML tags or formatting with it. (e.g. Hello).
  3. HTML is also text but, unlike plain text, it does indeed get parsed as markup when it’s added to the DOM (e.g. <div>Hello</div>).

It might help to map out exactly which parameters are supported by which methods:

Method Node HTML Text Text
append Yes No Yes
appendChild Yes No No
insertAdjacentHTML No Yes Yes1
innerHTML2 No Yes Yes
1 This works, but insertAdjacentText is recommended.
2 Instead of taking traditional parameters, innerHTML is used like: element.innerHTML = 'HTML String'

How to choose which method to use

Well, it really depends on what you’re looking to append, not to mention certain browser quirks to work around.

  • If you have existing HTML that gets sent to your JavaScript, it’s probably easiest to work with methods that support HTML.
  • If you’re building some new HTML in JavasScript, creating a node with heavy markup can be cumbersome, whereas HTML is less verbose.
  • If you want to attach event listeners right away, you’ll want to work with nodes because we call addEventListener on nodes, not HTML.
  • If all you need is text, any method supporting plain text parameters is fine.
  • If your HTML is potentially untrustworthy (i.e. it comes from user input, say a comment on a blog post), then you’ll want to be careful when using HTML, unless it has been sanitized (i.e. the harmful code has been removed).
  • If you need to support Internet Explorer, then using append is out of the question.


Let’s say we have a chat application, and we want to append a user, Dale, to a buddy list when they log in.

<!-- HTML Buddy List --> <ul id="buddies">   <li><a>Alex</a></li>   <li><a>Barry</a></li>   <li><a>Clive</a></li>   <!-- Append next user here --> </ul>

Here’s how we’d accomplish this using each of the methods above.


We need to create a node object that translates to <li><a>Dale</a></li>.

const new_buddy = document.createElement('li') const new_link = document.createElement('a')  const buddy_name = "Dale"  new_link.append(buddy_name) // Text param new_buddy.append(new_link) // Node param  const list = document.querySelector('#buddies') list.append(new_buddy) // Node param

Our final append places the new user at the end of the buddy list, just before the closing </ul> tag. If we’d prefer to place the user at the front of the list, we could use the prepend method instead.

You may have noticed that we were also able to use append to fill our <a> tag with text like this:

const buddy_name = "Dale" new_link.append(buddy_name) // Text param

This highlights the versatility of append.

And just to call it out once more, append is unsupported in Internet Explorer.


appendChild is another JavaScript method we have for appending stuff to DOM elements. It’s a little limited in that it only works with node objects, so we we’ll need some help from textContent (or innerText) for our plain text needs.

Note that appendChild, unlike append, is supported in Internet Explorer.

const new_buddy = document.createElement('li') const new_link = document.createElement('a')  const buddy_name = "Dale"  new_link.textContent = buddy_name new_buddy.appendChild(new_link) // Node param  const list = document.querySelector('#buddies') list.appendChild(new_buddy) // Node param

Before moving on, let’s consider a similar example, but with heavier markup.

Let’s say the HTML we wanted to append didn’t look like <li><a>Dale</a></li>, but rather:

<li class="abc" data-tooltip="Click for Dale">   <a id="user_123" class="def" data-user="dale">     <img src="images/dale.jpg" alt="Profile Picture"/>     <span>Dale</span>   </a> </li>

Our JavaScript would look something like:

const buddy_name = "Dale"  const new_buddy = document.createElement('li') new_buddy.className = ('abc') new_buddy.setAttribute('data-tooltip', `Click for $ {buddy_name}`)  const new_link = document.createElement('a') new_link.id = 'user_123' new_link.className = ('def') new_link.setAttribute('data-user', buddy_name)  const new_profile_img = document.createElement('img') new_profile_img.src = 'images/dale.jpg' new_profile_img.alt = 'Profile Picture'  const new_buddy_span = document.createElement('span') new_buddy_span.textContent = buddy_name  new_link.appendChild(new_profile_img) // Node param new_link.appendChild(new_buddy_span) // Node param new_buddy.appendChild(new_link) // Node param  const list = document.querySelector('#buddies') list.appendChild(new_buddy) // Node param

There’s no need to follow all of above JavaScript – the point is that creating large amounts of HTML in JavaScript can become quite cumbersome. And there’s no getting around this if we use append or appendChild.

In this heavy markup scenario, it might be nice to just write our HTML as a string, rather than using a bunch of JavaScript methods…


insertAdjacentHTML is is like append in that it’s also capable of adding stuff to DOM elements. One difference, though, is that insertAdjacentHTML inserts that stuff at a specific position relative to the matched element.

And it just so happens to work with HTML. That means we can insert actual HTML to a DOM element, and pinpoint exactly where we want it with four different positions:

<!-- beforebegin --> <div id="example" class="group">   <!-- afterbegin -->   Hello World   <!-- beforeend --> </div> <!-- afterend -->

So, we can sorta replicate the same idea of “appending” our HTML by inserting it at the beforeend position of the #buddies selector:

const buddy_name = "Dale"  const new_buddy = `<li><a>$ {buddy_name}</a></li>` const list = document.querySelector('#buddies') list.insertAdjacentHTML('beforeend', new_buddy)

Remember the security concerns we mentioned earlier. We never want to insert HTML that’s been submitted by an end user, as we’d open ourselves up to cross-site scripting vulnerabilities.


innerHTML is another method for inserting stuff. That said, it’s not recommended for inserting, as we’ll see.

Here’s our query and the HTML we want to insert:

const buddy_name = "Dale" const new_buddy = `<li><a>$ {buddy_name}</a></li>` const list = document.querySelector('#buddies')   list.innerHTML += new_buddy

Initially, this seems to work. Our updated buddy list looks like this in the DOM:

<ul id="buddies">   <li><a>Alex</a></li>   <li><a>Barry</a></li>   <li><a>Clive</a></li>   <li><a>Dale</a></li> </ul>

That’s what we want! But there’s a constraint with using innerHTML that prevents us from using event listeners on any elements inside of #buddies because of the nature of += in list.innerHTML += new_buddy.

You see, A += B behaves the same as A = A + B. In this case, A is our existing HTML and B is what we’re inserting to it. The problem is that this results in a copy of the existing HTML with the additional inserted HTML. And event listeners are unable to listen to copies. That means if we want to listen for a click event on any of the <a> tags in the buddy list, we’re going to lose that ability with innerHTML.

So, just a word of caution there.


Here’s a demo that pulls together all of the methods we’ve covered. Clicking the button of each method inserts “Dale” as an item in the buddies list.

Go ahead and open up DevTools while you’re at it and see how the new list item is added to the DOM.


Here’s a general overview of where we stand when we’re appending and inserting stuff into the DOM. Consider it a cheatsheet for when you need help figuring out which method to use.

Method Node HTML Text Text Internet Explorer? Event Listeners Secure? HTML Templating
append Yes No Yes No Preserves Yes Medium
appendChild Yes No No Yes Preserves Yes Medium
insertAdjacentHTML No Yes Yes1 Yes Preserves Careful Easy
innerHTML2 No Yes Yes Yes Loses Careful Easy
1 This works, but insertAdjacentText is recommended.
2 Instead of taking traditional parameters, innerHTML is used like: element.innerHTML = 'HTML String'

If I had to condense all of that into a few recommendations:

  • Using innerHTML for appending is not recommended as it removes event listeners.
  • append works well if you like the flexibility of working with node elements or plain text, and don’t need to support Internet Explorer.
  • appendChild works well if you like (or need) to work with node elements, and want full browser coverage.
  • insertAdjacentHTML is nice if you need to generate HTML, and want to more specific control over where it is placed in the DOM.

Last thought and a quick plug 🙂

This post was inspired by real issues I recently ran into when building a chat application. As you’d imagine, a chat application relies on a lot of appending/inserting — people coming online, new messages, notifications, etc.

That chat application is called Bounce. It’s a peer-to-peer learning chat. Assuming you’re a JavaScript developer (among other things), you probably have something to teach! And you can earn some extra cash.

If you’re curious, here’s a link to the homepage, or my profile on Bounce. Cheers!

The post Comparing Methods for Appending and Inserting With JavaScript appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , ,

Comparing the New Generation of Build Tools

A bunch of new developer tools have landed in the past year and they are biting at the heels of the tools that have dominated front-end development over the last few years, including webpack, Babel, Rollup, Parcel, create-react-app.

These new tools aren’t designed to perform the exact same function, and each has different things they’re trying to achieve and features to get there. Despite their differences, these tools do share a common goal: improve the developer experience.

Specifically, I’d like to evaluate each one, outlining what they do, why we need them, and their use cases. I realize that comparisons aren’t always fair. Again, it’s not like any of the things we’re looking at in this article are direct competitors. In fact, Snowpack and Vite actually use esbuild under the hood for certain tasks. Our goal is more to get a better view of the landscape of developer tools that run tasks to make our jobs easier. This way, we see what options are out there and how they stack up, so we can make the best choices when we need them.

Of course, all of this will be colored by my experience using React and Preact. I’m more familiar with these frameworks libraries, but we’ll look at their support for other front-end frameworks too.

There have been a whole lot of great articles, streams and podcasts about these new developer tools. There are a couple of ShopTalk Show episodes I’d recommend for more context: Episode 454 discusses Vite and Episode 448 features the creators of wmr and Snowpack. Something that stands out from these episodes is that a huge amount of work has gone into building these tools to modernize our developer environments.

Why are these tools all arriving now?

In part, I think these tools are arriving as a reaction to JavaScript tooling fatigue — something captured nicely in this article about learning JavaScript back in 2016. They also fill a missing middle ground between writing a single vanilla JavaScript file, and having to download 200 megabytes of tooling dependencies before you’ve written a line of your own code. They come batteries-included without the dependency list, and are part of a trend of collapsing layers in the JavaScript ecosystem.

Snowpack, Vite, and wmr have all been enabled by native JavaScript modules in the browser. Back in 2018, Firefox 60 was released with ECMAScript 2015 modules enabled by default. Since then, all major browser engines have supported native JavaScript modules. Node.js also shipped with native JavaScript modules in November 2019. We’re still finding out what possibilities native JavaScript modules unlock today in 2021.

How are these different from existing tools?

Whether we use webpack, Rollup, or Parcel for a development server, the tool bundles our entire codebase from our source code and a node_modules folder, runs these through build processes —  like Babel, TypeScript, or PostCSS — then pushes the bundled code to our browser. This all takes work, and can slow development servers to a crawl in larger codebases, even after all the work that’s gone into caching and optimizing.

Snowpack, Vite, and wmr development servers don’t follow this model. Instead, they wait until the browser finds an import statement and makes an HTTP request for the module. Only after this request is made will the tool apply transforms to the requested module and any leaf nodes in the module’s import tree, then serve these to the browser. This speeds things up a lot as there’s less work in the process of pushing to a dev server.

You’ll notice esbuild missing from this picture. It’s a bundler first and foremost. It doesn’t side-step bundling the way the other tools do. Instead, esbuild processes code extremely fast by avoiding expensive transformations, leveraging parallelization and using the Go language.

The experiment

I took one of the the example apps from the React docs and rebuilt it with each tool covered in this article. The project I went with was Snap Shot by Yogita Verma. Here’s a link to the original repo, and a link to my repo with the four versions of Snap Shot, each using a different build tool. We’ll compare the output of each build step later. Rebuilding this app allowed me to test out the developer experience of pulling some pretty standard React dependencies into the tools, including React Router and axios.

Comparable features

Before we get into the specifics of each individual tool, they all support the following features out of the box (to varying degrees):

  • First-class support for native JavaScript modules
  • TypeScript compilation (but not type checking)
  • JSX
  • Plugin API for extensibility
  • A built-in development server
  • CSS bundling and support for CSS-in-JS libraries

All of these tools can compile TypeScript into JavaScript, but will do so even if there are type errors. For proper type checking you would need to install TypeScript and run tsc --noEmit on your root JavaScript file, or alternatively, use editor plugins to watch for type errors.

OK, let’s take look at each tool.


esbuild was created by Evan Wallace (CTO of Figma). Its main feature is that it provides a build step 10×-100× faster than Node-based bundlers (by their own benchmarks). It doesn’t provide many of the developer conveniences you might find in something like create-react-app. But there are more and more esbuild starters popping up that fills those gaps, including create-react-app-esbuild, estrella and Snowpack, which uses esbuild for its build step.

esbuild is very new. It hasn’t yet reached a 1.0 version and isn’t quite ready for production use — but it’s not far off. It gives you intuitive JavaScript and command line APIs with smart defaults.

Use cases

esbuild is a complete game-changer in the bundler world. It’s going to be most useful in large codebases where the speed difference between esbuild and node bundlers gets multiplied. When esbuild hits 1.0 it’s going to be very useful in big production sites, and will save teams a whole lot of time waiting for builds to complete. Unfortunately, big production sites will have to wait until esbuild becomes stable. In the meantime it’ll just be good to add some speed to your bundling in side projects.

esbuild’s lightening fast speed will be a bonus for any kind of work that you’re doing. Less time spent waiting for builds to run is always going to be good for developer experience! This considered, if you’re prototyping quick applications you might want to start with something more high level than esbuild — otherwise, you’ll need to spend some time pulling in dependencies and configuring your environment before you get conveniences we expect in the JavaScript ecosystem. Also, if you want to minimize the size of your bundle as much as possible you may want to use Rollup and terser, which will produce slightly smaller bundle sizes.


I decided to start a React project in esbuild in a naïve way: npm installing esbuild, React and ReactDOM. I created a src/app.jsx file and a dist/index.html file. Then, I used the following command to compile the app into a dist/bundle.js file:

./node_modules/.bin/esbuild src/app.jsx --bundle --platform=browser --outfile=dist/bundle.js

When I hosted and opened index.html in the browser, I was met with the “white screen of death” and an “Uncaught ReferenceError: process is not defined” console error. Both the docs and the CLI explain exactly what you need to do to prevent this but it might be a bit of a “gotcha” for beginners, because it requires an extra argument when bundling React:


Or, if you’re including esbuild in npm scripts written like this to escape the quotes:


This define argument is needed for any library bundled for the browser that expects node environment variables. Vue 2.0 also expects these. You won’t have the same problem with Preact because it doesn’t expect any environment variables and ships ready for the browser by default.

After I ran the command with the define argument, my “Hello world” React app was working perfectly. JSX works out of the box with .jsx files. That said, React needs to be manually imported and then JSX is converted to the React.createElement. However, there are ways to add auto imports in JSX and/or configure JSX for Preact.


esbuild provides a --serve option for a development server. This bypasses the filesystem and serves modules straight from memory, ensuring that the browser doesn’t pull older versions of modules. However, it doesn’t include live/hot reloading, so you will find yourself refreshing the browser after saving which isn’t an ideal experience.

I decided to use the newly-released watch feature.This tells esbuild to recompile code every time a source file is saved. But we still need a server to see our saved changes. We can pull in a development server package, such as Luke Jackson’s servor:

npm install servor --save-dev

Then we can use the esbuild Javascript API to start as server and run esbuild’s watch mode at the same time. Let’s create a file at the root of our project called watch.js:

// watch.js const esbuild = require("esbuild"); const servor = require("servor");  esbuild.build({   // pass any options to esbuild here...   entryPoints: ["src/app.jsx"],   outdir: "dist",   define: { "process.env.NODE_ENV": '"production"' },   watch: true, });  async function serve(){   console.log("running server from: http://localhost:8080/");   await servor({     // pass any options to servor here...     browser:true,     root: "dist",     port: 8080,   }); }  serve();

Now run node watch.js in the command line. This gives us a nice dev server, though again, it doesn’t give us hot module replacement or fast refresh (i.e., your client-side state won’t be preserved). But this was enough for my testing needs.

Even though we’re rebundling our entire application every time we save a file, we’d need to have a pretty massive application before esbuild slows down. After I set up this tooling, I was getting instant feedback from changes. My computer uses an intel i7 from 2012, so it certainly isn’t a top-of-the-line machine.

If you need a preconfigured version of esbuild with live reload and some React defaults, you can clone this repo.

Supported files

esbuild can import CSS in JavaScript if that’s your style. It will compile CSS into an output file with the same name as your main output JavaScript file. It can also bundle CSS @import statements by default. There is no support for CSS Modules, but there are plans for it.

There is a growing community of plugins for esbuild. For example, there are plugins available for Vue single file components, and Svelte components.

esbuild works with JSON files and can bundle them into JavaScript modules without any configuration.

It can also import images in JavaScript with the option to either convert them into data URLs or copying them into an output folder. This behavior isn’t enabled by default, but you can add the following in your esbuild config object to enable either option:

loader: { '.png': 'dataurl' } // Converts to data url in JS bundle loader: { '.png': 'file' } // Copies to output folder

Code splitting appears to be a work in progress, but is mostly there in the ESM output format, and it does look like it is a priority for the project. It’s also worth mentioning that tree-shaking is built into esbuild by default and can’t be turned off.

Production build

Using the “minify” and “bundle” options in your esbuild command won’t create a bundle quite as small as a Rollup/Terser pipeline. This is because esbuild sacrifices some bundle size optimization to get through your code in as few passes as possible. However, the difference may be pretty negligible, and worth it for the increase in bundling speed, depending on your project. In my clone of the Snap Shot application, esbuild created a bundle of 177 KB which isn’t a lot more than the 165KB produced by Vite, which uses rollup and terser.


Templates for multiple front end frameworks
Hot module replacement development server
Streaming imports
Preconfigured production build 
Automatic PostCSS and preprocessor conversion
HTM transform
Rollup plugin support
Size on disk (default install) 7.34 MB

esbuild is an extremely powerful tool. But it might be difficult if you’re used to zero-config setups. If you need more, then you might want to take a look at the next tool, Snowpack, which uses esbuild.


Snowpack is a build tool by the creators of Skypack and Pika. It provides an awesome development server and was created with an “unbundled development” philosophy. To quote the documentation: “You should be able to use a bundler because you want to, and not because you need to.”

By default, Snowpack’s build step doesn’t bundle files into a single package but provides unbundled esmodules that run in the browser. esbuild is actually included in there as a dependency, but the idea is to use JavaScript modules and only bundle with esbuild when it’s needed.

Snowpack has some pretty slick documentation, including a list of guides for using it with JavaScript frameworks, and a bunch of templates for them. Some of the guides are still a work in progress, but others like the one for React are nice and clear. It also looks like Snowpack treats Svelte as a first-class citizen. I actually first heard about Snowpack from Rich Harris’s “Futuristic Web Development” talk at Svelte Summit 2020. That said, the upcoming Svelte meta-framework SvelteKit was supposed to be powered by Snowpack but has since switched to Vite (which we’ll review next).

Use cases

Snowpack is a good choice if you want to double down on unbundled deployment. You may be writing source code with a small number of modules. This would mean you’re not creating a big request waterfall with an unbundled build. If you don’t need the added complexity and technical debt of bundling, then Snowpack is a great choice. A good use case would be if you’re incrementally adopting a front-end framework into a server-rendered or static application. You’d be pulling in as little tooling as possible from the node ecosystem but you’d still be getting the benefits of declarative frontend frameworks.

Secondly, I’d argue that Snowpack is a great wrapper around esbuild. If you want to try out esbuild but also want a development server and pre-written templates for front-end frameworks, then you can’t go wrong with Snowpack. Enable esbuild in the build step of your Snowpack config and you’re good to go.

As things currently stand, I’d argue that Snowpack wouldn’t be the best replacement for a zero configuration tool like create-react-app because you’ll need to pull in plugins and configure them yourself if you have a big application and need a super-fancy optimized production-ready build step.


Let’s start a project with Snowpack by jumping into the command line:

mkdir snowpackproject cd snowpackproject npm init #fill with defaults  npm install snowpack

Now, let’s add the following to package.json:

// package.json "scripts": {   "start": "snowpack dev",   "build": "snowpack build" },

Next, we’ll create a configuration file:

// Mac or Linux touch snowpack.config.js // Windows new-item snowpack.config.js

I think the most magical part of Snowpack comes when setting one innocent-looking key value pair in the configuration file. Paste this into the configuration file, for example:

// snowpack.config.js module.exports = {   packageOptions: {     "source": "remote",   } };

source: remote enables something called streaming imports. Streaming imports enable Snowpack to bypass npm installation by converting bare imports (e.g., import React from 'react';) into CDN imports from Skypack.

Moving ahead, let’s make an index.html file:

<!--index.html--> <!DOCTYPE html> <html lang="en"> <head>   <meta charset="UTF-8">>   <title>Snowpack streaming imports</title> </head> <body>   <div id="root"></div>   <!-- Note the type="module". This is important for JavaScript module imports. -->   <script type="module" src="app.js"></script> </body> </html>

And, finally, we’ll add an app.jsx file:

// app.jsx import React from 'react' import ReactDOM from 'react-dom' const App = ()=>{   return <h1>Welcome to Snowpack streaming imports!</h1> } ReactDOM.render(<App />,document.getElementById('root')); 0

Note that we didn’t npm install React or ReactDOM at any stage. But if we start up the Snowpack developer server like this:

./node_modules/.bin/snowpack dev

…our app still works!

Instead of pulling from a node_modules folder, Snowpack pulls the npm package down from Skypack, a CDN that hosts the npm registry, and it’s is pre-optimized to work in the browser. Snowpack then serves it in a ./_snowpack/pkg URL.


This is a big step away from Node/npm-based workflow. What we’re actually looking at is a new CDN/JavaScript module-based workflow.

If, however, we our app as it is and run a production build, Snowpack throws an error. This is because it needs to know which versions of React and ReactDOM to use when building. You can fix this by writing to a snowpack.deps.json which can automatically be created by running the following:

./node_modules/.bin/snowpack add react ./node_modules/.bin/snowpack add react-dom

That won’t download the package from npm, but it will record the version of the packages used for Snowpack builds.

One caveat is that we miss out on developer error messages, as Skypack will ship the production version of packages.

Even if we aren’t using streaming imports, the Snowpack development server bundles each dependency from node_modules into one JavaScript file per dependency, converts those files to a native JavaScript module, then serves it to the browser. This means the browser can cache these scripts and only re-request them if they’ve changed. The development server automatically refreshes on save, but doesn’t preserve the client-side state. All dependencies from node seemed to work out of the box regardless of whether they were using legacy module formats or node APIs (such as the infamous process.env we had trouble with in esbuild).

Preserving client-side state in React requires react-refresh, which requires a few Babel packages of its own as dependencies. These aren’t included by default, but are available using the more maximal React template. The template pulls in react-refresh, Prettier, Chai, and React Testing Library, for a total Node dependency package weighing in at 80 MB:

npx create-snowpack-app my-react-project --template @snowpack/app-template-react

Supported files

JSX is supported, but again, only with .jsx files by default. Snowpack automatically detects if whether React or Preact is being used, and decides accordingly which render function to use for the JSX transform. However, if we want to customize the JSX further than this, we’d need to pull in Babel via their plugin. There is also a Snowpack plugin available for Vue single file components and, of course, for Svelte components. Further, Snowpack compiles TypeScript, but for type checking we need the TypeScript plugin.

CSS can be imported into JavaScript and are tossed into the document <head> at runtime. CSS modules are also supported out of the box for scoping as long as they have the .module.css extension.

Imported JSON files will be cast into a JavaScript module with an object as a default export. Snowpack supports images and copies them into the production folder. Going along with its unbundled philosophy, Snowpack does not include images as data URLs in the bundle.

Production build

The default snowpack build command basically copies the exact source file structure into an output folder. For files that compile to JavaScript (e.g. TypeScript, JSX, JSON, .vue, .svelte), it transforms each individual file into a separate browser-friendly JavaScript module.

This works fine, but isn’t great for production, as it could cause a big waterfall of requests if the source code is split into a lot of files. In the Snap Shot application I ended up with 184KB of source files which would then request another 105 KB of dependencies from Skypack which made for a pretty huge waterfall.

However, Snowpack pulls esbuild as a dependency and we can enable esbuild to bundle, minify and compile our code by adding an “optimize” object to the Snowpack config:

// snowpack.config.js module.exports = {   optimize: {     bundle: true,     minify: true,     target: 'es2018',   }, };

This runs the code using the optimization features provided by esbuild, so by just adding these options we could get the same build we had earlier on with esbuild.

Since esbuild hasn’t reached 1.0 yet, Snowpack recommends using either the webpack or Rollup plugin for production builds, both of which need to be configured.


Snowpack provides a lightweight developer experience with a full-featured development server, detailed documentation, and easy-to-install templates. You are left to decide whether you want to bundle your application and how you want to do so. If you want a tool that provides both a dev server and a more opinionated build step, you might want to take a look at Vite, the next tool on our list.

Templates for multiple front end frameworks
Hot module replacement development server ✅ (when using templates)
Streaming imports
Preconfigured production build 
Automatic PostCSS and preprocessor conversion
HTM transform
Rollup plugin support ✅ (when using snowpack-plugin-rollup-bundle for build step)
Size on disk (default install) 16 MB


Vite is developed by Vue creator (and Hades speedrunner) Evan You. Where esbuild concentrates on the build step and Snowpack concentrates on the development server, Vite provides both: a full development server and an optimized build command using Rollup.

Use cases

If you want a serious create-react-app or Vue CLI competitor, Vite is the closest one in the bunch because it comes with batteries-included features. The lightening-fast development server and zero-config optimized production build mean you can get from zero to production without any configuration. Vite is a tool that could be used in both a tiny side-project or a big production application. A good use case for Vite would be any sizeable single page app.

Why wouldn’t you use Vite? Vite is an opinionated tool and you might disagree with its opinions. You might not want to use Rollup for your build (we’ve been talking about how fast esbuild is), or you might want your tooling to give you the full power of Babel, eslint and the ecosystem of webpack loaders out of the box.

Also, you want zero-config server-side rendering meta-frameworks, you’d be better off staying with webpack-based frameworks, like Nuxt.js and Next.js until the story for Vite server-side rendering is more complete.


Vite has more opinionated defaults than esbuild and Snowpack. Its documentation is clear and detailed. We get full support for Vue with Evan being the creator and all, so Vite is a definite happy path for Vue developers. That said, Vite can be used with any front-end framework and even provides a list of templates to get you started.


Vite’s development server is pretty powerful. Vite pre-bundles all of a project’s dependencies together into a single native JavaScript module with esbuild, then serves it up with a heavily cached HTTP header. This means no time is wasted on compiling, serving or requesting imported dependencies after the first page load. Vite also provides clear error messaging, printing the exact block of code and the line numbers to troubleshoot. Again with Vite, I didn’t have any issues pulling in dependencies that used node APIs or legacy formats. They all seemed to be shimmed into a browser-acceptable esmodule.

Vite’s React and Vue templates both pull in plugins that enable hot module replacement. The Vue template pulls in a Vue plugin for single file components, and a Vue plugin for JSX. The React template pulls in the react-refresh plugin. Either way, both will give you hot module replacement and client-side state preservation. Sure, they add a few more dependencies, including Babel packages, but, Babel isn’t actually necessary when using JSX in Vite. By default, JSX works the same way as esbuild — it converts to React.createElement. It won’t automatically import React, but its behavior can be configured.

And while we’re at it, Vite doesn’t support streaming imports like Snowpack and wmr do. That means npm-installing dependencies as usual.

One cool thing is that Vite includes experimental support for server-side rendering. Pick your framework of choice and generate static HTML that ships directly to the client. At the moment, it looks like we need to construct this architecture on our own, but still, this looks like a good opportunity for meta-frameworks to be built on top of Vite. Evan You already has a work in progress called VitePress, a replacement for VuePress with the benefits of using Vite. And Sveltekit has also added Vite to its dependency list. It looks like CSS code splitting inclusion was part of the reason Sveltekit made the switch to Vite.

Supported files

For CSS, Vite provides the most features out of all of the tools that we are looking at. It supports bundling CSS imports as well as CSS modules. But we can also npm install PostCSS plugins and create a postcss.config.js file, and Vite will automatically start applying these transforms to CSS.

We can install and use CSS preprocessors — simply npm install the preprocessor and rename the file to the right extension (e.g. .filename.scss) and Vite will start applying the corresponding preprocessor. And, as we said in the overview, Vite support CSS code-splitting.

Image imports default to a public URL, but we’re also able load them into the bundle as strings by using a ?raw parameter at the end of the URL string.

JSON files can be imported in the source and converted into an esmodule exporting a single object. We can also provide a named import and Vite will look in the root field of the JSON file to find the import and treeshake the rest.

Production build

Vite uses Rollup for a preconfigured production build with a bunch of optimizations. It intentionally provides a zero-config build which should be enough for most use cases.

The build comes with the Rollup features we expect: bundling, minification and tree shaking. But we also get extras, like code-splitting dynamic imports and something called “asynchronous chunk loading” which is a fancy way to say that if we request a JavaScript module that imports another module, the build will be pre-optimized to load both at the same time (asynchronously).

Running Vite’s default build with the Snap Shot app I ended up with one 5KB JavaScript file and one 160KB JavaScript file (for a grand total of 165KB) and all CSS in the project was automatically minified to a tiny 2.71KB file.


The opinionated nature of Vite makes it a serious competitor with our current tooling. A lot of work has been done to make the developer experience really seamless and make production-ready builds out of the box.

Templates for multiple front end frameworks
Hot module replacement development server ✅ (when using templates)
Streaming imports
Preconfigured production build 
Automatic PostCSS and preprocessor conversion
HTM transform
Rollup plugin support ✅ 
Size on disk (default install) 17.1 MB


Like Vite, wmr is another opinionated build tool that provides both a development server and a build step. It was built by the creator of Preact, Jason Miller, so it’s definitely a happy path for Preact developers. Jason Miller explained the thinking behind wmr when he appeared as a guest on the JS Party podcast:

Preact is tiny and it’s really good if you want to do a lightweight project. Where is our tooling for that? We have a webpack-based tool that’s used in used in production by a bunch of high profile sites, but that’s the heavyweight tool. Where’s the prototyping tool? That was the one hand. The other hand is myself and a bunch of others who happened to be on the Preact team; We had been kind of on the sidelines of the bundler ecosystem for a little while, prodding people, trying to get consensus on a direction that we can move in to further this idea of writing modern code and shipping modern code.

This tells us that wmr is all about writing and shipping modern code, enabling lighter tooling in a project.

You might be wondering what wmr stands for? Nothing! The names “Web Modules Runtime” and “Wet Module Replacement” were floated, but it’s a fake abbreviation, similar to npm.

wmr is built with the same ruthless bundle size purging as Preact, so it’s tiny — weighing a mere 2.6 MB — and contains exactly zero npm dependencies. Still, it manages to pack in a whole lot of really awesome features including a hot-module-replacing development server and an optimized production build.

Use cases

I would use wmr if I was looking to create a prototype using Preact as fast as possible. There’s no configuration and it only takes seconds to download. It feels like using a supercharged static file server. With TypeScript, an optimized-build step, and static HTML rendering, wmr offers everything needed to ship small-to-medium sized applications. Its small size is also great for quickly trying out a library or demoing an idea.

wmr may not be the tool for you if you’re not using Preact, React or vanilla JavaScript. The Preact team has yet to provide templates for other frameworks. The documentation also isn’t as detailed as the other tools we’ve looked at. This means the further you stray from the happy path, the more you’ll dig into the source. So, I can’t recommend it if a lot of customization is needed.


If you’re using preact there is absolutely zero setup needed except for a quick npm install. To use React with wmr instead of Preact, there are currently two steps to take. First, alias htm/preact to htm/react, and react to es-react in your package.json:

"alias": {   "htm/preact": "htm/react",   "react": "es-react" },

Then add imports from es-react into your components:

// ReactDOM only needed on root render import { React, ReactDOM,} from 'es-react';

This means we aren’t actually using the normal React package you might be used to, but instead pulling in React from es-react. This is because wmr relies on packages being compatible with native JavaScript modules. React does not use native modules by default, instead using an older style of module called UMD modules. es-react is a package that pulls in React but provides exports that are compatible with the web platform.

This illustrates wmr’s philosophy of using native primitives of the web platform as opposed to using tooling to sidestep and abstract it away.

Another option could be to use Skypack imports in our application, which is also pre-optimized to work in the browser:

import React from 'https://cdn.skypack.dev/react'; import ReactDOM from 'https://cdn.skypack.dev/react-dom';

wmr expects that you’re writing modern code which runs in the browser, which may mean that you’ll need to do some configuration if you pull in dependencies that use node APIs or legacy module systems. To get the Snap Shot app working I needed to dive into node modules and convert a library or two to use native JavaScript module syntax. This might slow you down if you are using older libraries. The Preact ecosystem is all optimized to run in the browser and shouldn’t require any massaging. This is another reason to stick with the Preact happy path in wmr.

There are plugins for wmr. It exposes a plugin API that supports Rollup plugins for a build step. There are more and more wmr-specific examples on the docs, including a plugin that minifies HTML, and one that features filesystembased routing.

wmr supports different frameworks, but there aren’t any pre-built templates for them. And at first I found it rather difficult to configure the JSX transform. All that said, Jason has confirmed there are plans to make JSX more configurable and that wmr is intended to be framework-agnostic. JSX is planned to work out of the box in regular JavaScript files.


To get started you can either run this command in the command line:

npm init wmr your-project-name

Or alternatively, you can run these commands to manually build up your application:

npm init -y npm install wmr mkdir public touch public/index.html touch public/index.js 

Then add an script import in the body of your index.html (again make sure to use type="module"):

<script type="module" src="./index.js"></script>  

Now you can write a Preact hello world into your index.js file:

import { render } from 'preact'; render(<h1>Hello World!</h1>, document.body);

And finally start your development server:


Now we have a full hot module replacement development server which will immediately respond to any changes to our source-code.

wmr uses a tool called htm when transforming JSX, which provides some awesome benefits. Let’s say we’re writing a counter using Preact in wmr and make a mistake:

import { render } from 'preact'; import { useState } from 'preact/hooks'; function App() {   const [count,setCount] = useState(0)   return <>   <button onClick={()=>{setCount(cout+5)}}>Click to add 5 to count</button> // HIGHLIGHT   <p>count: {count}</p>   </> } render(<App />, document.body);

count is misspelled in the onClick handler function, so running this results in an error. Usually, we would have to rely on our tooling and a source map to gather information about where the bug is located, but wmr takes a different solution. With htm, this gets as close as you can get to native JSX in the browser by using tagged template literals. So, where writing React or Preact code usually looks like this:

<MyComponent>I am JSX. I am not actually valid Javascript</MyComponent>

…htm looks more like this:

html`<$ {MyComponent}>I am about as close as it gets to JSX as you can get while being able to run in the browser</MyComponent>`

Now, if we’re debugging our code and open the “Sources” panel in DevTools, we should see a script that’s almost identical to what the source code looks like in the editor.

Image of the source code.

This way, we can properly investigate where bugs are located in the browser without having to use sourcemaps. Sure, this specific example is pretty contrived, but you can see how this could be really useful because it means wmr doesn’t need source maps in your developer environment.

wmr supports streaming imports by default, so bare imports will be pulled down from the npm registry. This is done through a complex process which examines all of the source in the npm package, removes all the tests and metadata, and converts it to a single native JavaScript import. Similar to Snowpack, it’s possible to build a complex app without using npm to install anything. In fact, wmr is the first tool to support this idea.

Supported files

As far as the other types of files wmr supports, CSS files can be imported in JavaScript, and CSS modules are supported as well.

There isn’t any built-in support for either Vue single file components or Svelte components. However, wmr’s build step works with Rollup plugins and the development server can be configured with Polka/Express middleware, so it’s possible to use these to convert imports into Vue and Svelte components. In fact, I wrote a small plugin for Vue Single file Components to show how this might be done.

We can’t import images into JavaScript as data URLs in wmr without a plugin. Instead, we need to import them using a syntactically correct JavaScript method. So, if we have, say, a picture of a dog in our public folder we might include it in a Preact component like this:

function Dog() {   return <img src={new URL('./dog.jpg', import.meta.url)} alt="dog hanging out"></img> }

And once the build step runs, the image is copied and accessible from the distribution folder. There is hot module replacement for images in the development server, so changes with images are immediately reflected in the browser.

One more note on file support: JSON can be imported, and is converted into a JavaScript object for use. But when actually building an application, we’d need the Rollup JSON plugin.

Production build

wmr provides a production build step that includes bundling, minification and tree-shaking without any additional dependencies. Having a look at the source of wmr, it looks like rollup and terser are used under the hood, and minified versions of these are included in the wmr package. The wmr bundle for the Snap Shot app was 164KB so it created a bundle just a tiny bit smaller than total size of the two JavaScript files created by Vite.

There is also a way to configure wmr is such a way that it renders an application to static HTML and hydrates on the browser using preact-iso. This means wmr can be used as a meta-framework for Preact, similar to Next.js.


I love the experience of using wmr to prototype both React and Preact applications. It feels great to get started with a tool that is ridiculously small but provides developer conveniences that get close to matching heavyweight bundlers.

Templates for multiple front end frameworks
Hot module replacement development server
Streaming imports
Preconfigured production build 
Automatic PostCSS and preprocessor conversion
HTM transform
Rollup plugin support ✅ 
Size on disk (default install) 2.57 MB

Feature comparison

We just covered a lot ground! Rather than scroll up and down this article to compare results, I’ve compiled everything here to see how the tools stack up wen they’re side-by-side. I’ve even included additional comparisons for features that we didn’t explicitly covers.

Use cases

Tool Use case
esbuild Large codebases. Not ready for production yet.
Snowpack Small applications that don’t need bundling or applications where you want to choose which bundler you use. Also good for incrementally adopting JavaScript frameworks in server-rendered apps. 
Vite Replacement for Vue CLI/Create-React-App for producing single page applications. This is the happy path for Vue.
wmr Prototypes. Good for small- to medium-size apps and can be used for either single page or server-rendered apps. This is the happy path for Preact.


esbuild Snowpack Vite wmr
Templates for multiple front end frameworks
Size on disk (default install) 7.34 MB 16 MB 17.1 MB 2.57 MB
Zero-config production build
HMR Development Server with zero config
Process.env handling for node packages

Development server

esbuild Snowpack Vite wmr
Hot module replacement
CSS hot replacement
npm dependency pre-bundling
Browser error messaging
HTM tranform

Production build

esbuild Snowpack Vite wmr
Output bundle size of Snap Shop app 177 KB 184 KB of multiple JavaScript files, plus 105 KB of Skypack CDN dependencies  165 KB (one 5 KB file and one 160 KB file) 164 KB
Go-based bundling ✅ when using esbuild is used in build step
Preconfigured production build
Asynchronous chunk loading
Rollup plugin support

Other features

esbuild Snowpack Vite wmr
Streaming inputs
Server-side rendering ✅ (experimental)
CSS Modules
Automatic PostCSS and preprocessor conversion

Wrapping up

I’m super excited about building JavaScript applications with any and all of the tools we just looked at. Whether we’re writing a small side project or a big production site, all these tools speed up feedback loops and increase productivity. They’ve opened up the gates to ask what’s necessary in the JavaScript ecosystem and whether we can start losing the cruft brought by legacy modules and browsers. These tools are going to lower the barrier-to-entry for new developers by providing a leaner, faster developer environment with fewer abstractions between the code that gets written and the code that runs in the browser.

If you’re getting sick of waiting for your dependencies to download and your build steps to run, I’d recommend giving this new generation of tooling a try.

Further reading

Other new JavaScript tooling to check out

  • Rome – A full toolchain including linting, compiling, bundling, test-running and formatting
  • SWC – A rust-based JavaScript/TypeScript compiler
  • Deno – A runtime for JavaScript and TypeScript (similar to Node.js)

The post Comparing the New Generation of Build Tools appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , ,

Comparing Static Site Generator Build Times

There are so many static site generators (SSGs). It’s overwhelming trying to decide where to start. While an abundance of helpful articles may help wade through the (popular) options, they don’t magically make the decision easy.

I’ve been on a quest to help make that decision easier. A colleague of mine built a static site generator evaluation cheatsheet. It provides a really nice snapshot across numerous popular SSG choices. What’s missing is how they actually perform in action.

One feature every static site generator has in common is that it takes input data, runs it through a templating engine, and outputs HTML files. We typically refer to this process as The Build.

There’s too much nuance, context, and variability needed to compare how various SSGs perform during the build process to display on a spreadsheet — and thus begins our test to benchmark build times against popular static site generators.

This isn’t just to determine which SSG is fastest. Hugo already has that reputation. I mean, they say it on their website — The world’s fastest framework for building websites — so it must be true!

This is an in-depth comparison of build times across multiple popular SSGs and, more importantly, to analyze why those build times look the way they do. Blindly choosing the fastest or discrediting the slowest would be a mistake. Let’s find out why.

The tests

The testing process is designed to start simple — with just a few popular SSGs and a simple data format. A foundation on which to expand to more SSGs and more nuanced data. For today, the test includes six popular SSG choices:

Each test used the following approach and conditions:

  • The data source for each build are Markdown files with a randomly-generated title (as frontmatter) and body (containing three paragraphs of content).
  • The content contains no images.
  • Tests are run in series on a single machine, making the actual values less relevant than the relative comparison among the lot.
  • The output is plain text on an HTML page, run through the default starter, following each SSG’s respective guide on getting started.
  • Each test is a cold run. Caches are cleared and Markdown files are regenerated for every test.

These tests are considered benchmark tests. They are using basic Markdown files and outputting unstyled HTML into the built output.

In other words, the output is technically a website that could be deployed to production, though it’s not really a real-world scenario. Instead, this provides a baseline comparison among these frameworks. The choices you make as a developer using one of these frameworks will adjust the build times in various ways (usually by slowing it down).

For example, one way in which this doesn’t represent the real-world is that we’re testing cold builds. In the real-world, if you have 10,000 Markdown files as your data source and are using Gatsby, you’re going to make use of Gatsby’s cache, which will greatly reduce the build times (by as much as half).

The same can be said for incremental builds, which are related to warm versus cold runs in that they only build the file that changed. We’re not testing the incremental approach in these tests (at this time).

The two tiers of static site generators

Before we do that, let’s first consider that there are really two tiers of static site generators. Let’s call them basic and advanced.

  • Basic generators (which are not basic under the hood) are essentially a command-line interface (CLI) that takes in data and outputs HTML, and can often be extended to process assets (which we’re not doing here).
  • Advanced generators offer something in addition to outputting a static site, such as server-side rendering, serverless functions, and framework integration. They tend to be configured to be more dynamic right out of the box.

I intentionally chose three of each type of generator in this test. Falling into the basic bucket would be Eleventy, Hugo, and Jekyll. The other three are based on a front-end framework and ship with various amounts of tooling. Gatsby and Next are built on React, while Nuxt is built atop Vue.

Basic generators Advanced generators
Eleventy Gatsby
Hugo Next
Jekyll Nuxt

My hypothesis

Let’s apply the scientific method to this approach because science is fun (and useful)!

My hypothesis is that if an SSG is advanced, then it will perform slower than a basic SSG. I believe the results will reflect that because advanced SSGs have more overhead than basic SSGs. Thus, it’s likely that we’re going to see both groups of generators — basic and advanced — bundled together, in the results with basic generators moving significantly quicker.

Let me expand on that hypothesis a bit.

Linear(ish) and fast

Hugo and Eleventy will fly with smaller datasets. They are (relatively) simple processes in Go and Node.js, respectively, and their build output will reflect that. While both SSG will slow down as the number of files grows, I expect them to remain at the top of the class, though Eleventy may be a little less linear at scale, simply because Go tends to be more performant than Node.

Slow, then fast, but still slow

The advanced, or framework-bound SSGs, will start out and appear slow. I suspect a single-file test to contain a significant difference — milliseconds for the basic ones, compared to several seconds for Gatsby, Next, and Nuxt.

The framework-based SSGs are each built using webpack, bringing a significant amount of overhead along with it, regardless of the amount of content they are processing. That’s the baggage we sign up for in using those tools (more on this later).

But, as we add thousands of files, I suspect we’ll see the gap between the buckets close, though the advanced SSG group will stay farther behind by some significant amount.

In the advanced SSG group, I expect Gatsby to be the fastest, only because it doesn’t have a server-side component to worry about — but that’s just a gut feeling. Next and Nuxt may have optimized this to the point where, if we’re not using that feature, it won’t affect build times. And I suspect Nuxt will beat out Next, only because there is a little less overhead with Vue, compared to React.

Jekyll: The odd child

Ruby is infamously slow. It’s gotten more performant over time, but I don’t expect it to scale with Node, and certainly not with Go. And yet, at the same time, it doesn’t have the baggage of a framework.

At first, I think we’ll see Jekyll as pretty speedy, perhaps even indistinguishable from Eleventy. But as we get to the thousands of files, the performance will take a hit. My gut feeling is that there may exist a point at which Jekyll becomes the slowest of all six. We’ll push up to the 100,000 mark to see for sure.

A hand-drawn line chart showing build time on the y-axis and number of files on the x-asix, where Next is a green line, then nuxt is a yellow line, gatsby is a pink line jekyll is a blue line, eleventy is a teal line and hugo is an orange line. All lines show the build time increasing as the number of files increase, where jekyll has the sharpest slope.

The results are in!

The code that powers these tests are on GitHub. There’s also a site that shows the relative results.

After many iterations of building out a foundation on which these tests could be run, I ended up with a series of 10 runs in three different datasets:

  • Base: A single file, to compare the base build times
  • Small sites: From 1 to 1024 files, doubling each to time (to make it easier to determine if the SSGs scaled linearly)
  • Large sites: From 1,000 to 64,000 files, double on each run. I originally wanted to go up to 128,000 files, but hit some bottlenecks with a few of the frameworks. 64,000 ended up being enough to produce an idea of how the players would scale with even larger sites.

Click or tap the images to view them larger.

Summarizing the results

A few results were surprising to me, while others were expected. Here are the high-level points:

  • As expected, Hugo was the fastest, regardless of size. What I didn’t expect is that it wasn’t even close to any other generator, even at base builds (nor was it linear, but more on that below.)
  • The basic and advanced groups of SSGs are quite obvious when looking at the results for small sites. That was expected, but it was surprising to see Next is faster than Jekyll at 32,000 files, and faster than both Eleventy and Jekyll at 64,000 files. Also surprising is that Jekyll performed faster than Eleventy at 64,000 files.
  • None of the SSGs scale linearly. Next was the closest. Hugo has the appearance of being linear, but only because it’s so much faster than the rest.
  • I figured Gatsby to be the fastest among the advanced frameworks, and suspected it would be the one to get closer to the basics. But Gatsby turned out to be the slowest, producing the most dramatic curve.
  • While it wasn’t specifically mentioned in the hypothesis, the scale of differences was larger than I would have imagined. At one file, Hugo was approximately 170 times faster than Gatsby. But at 64,000 files, it was closer — about 25 times faster. That means that, while Hugo remains the fastest, it actually has the most dramatic exponential growth shape among the lot. It just looks linear because of the scale of the chart.

What does it all mean?

When I shared my results with the creators and maintainers of these SSGs, I generally received the same message. To paraphrase:

The generators that take more time to build do so because they are doing more. They are bringing more to the table for developers to work with, whereas the faster sites (i.e. the “basic” tools) focus their efforts largely in converting templates into HTML files.

I agree.

To sum it up: Scaling Jamstack sites is hard.

The challenges that will present themselves to you, Developer, as you scale a site will vary depending on the site you’re trying to build. That data isn’t captured here because it can’t be — every project is unique in some way.

What it really comes down to is your level of tolerance for waiting in exchange for developer experience.

For example, if you’re going to build a large, image-heavy site with Gatsby, you’re going to pay for it with build times, but you’re also given an immense network of plugins and a foundation on which to build a solid, organized, component-based website. Do the same with Jekyll, and it’s going to take a lot more effort to stay organized and efficient throughout the process, though your builds may run faster.

At work, I typically build sites with Gatsby (or Next, depending on the level of dynamic interactivity required). We’ve worked with the Gatsby framework to build a core on which we can rapidly build highly-customized, image-rich websites, packed with an abundance of components. Our builds become slower as the sites scale, but that’s when we get creative by implementing micro front-ends, offloading image processing, implementing content previews, along with many other optimizations.

On the side, I tend to prefer working with Eleventy. It’s usually just me writing code, and my needs are much simpler. (I like to think of myself as a good client for myself.) I feel I have more control over the output files, which makes it easier for me to get 💯s on client-side performance, and that’s important to me.

In the end, this isn’t only about what is fast or slow. It’s about what works best for you and how long you’re willing to wait.

Wrapping up

This is just the beginning! The goal of this effort was to create a foundation on which we can, together, benchmark relative build times across popular static site generators.

What ideas do you have? What holes can you poke in the process? What can we do to tighten up these tests? How can we make them more like real-world scenarios? Should we offload the processing to a dedicated machine?

These are the questions I’d love for you to help me answer. Let’s talk about it.

The post Comparing Static Site Generator Build Times appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

Comparing Various Ways to Hide Things in CSS

You would think that hiding content with CSS is a straightforward and solved problem, but there are multiple solutions, each one being unique.

Developers most commonly use display: none to hide the content on the page. Unfortunately, this way of hiding content isn’t bulletproof because now that content is now “inaccessible” to screen readers. It’s tempting to use it, but especially in cases where something is only meant to be visually hidden, don’t reach for it.

The fact is that there are many ways to “hide” things in CSS, each with their pros and cons which greatly depend on how it’s being used. We’re going to review each technique here and cap things off with a summary that helps us decide which to use and when.

How to spot differences between the techniques

To see a difference between different ways of hiding content, we must introduce some metrics. Metrics that we’ll use to compare the methods. I decided to break that down by asking questions focused on four particular areas that affect layout, performance and accessibility:

  1. Accessibility: Is the hidden content read by a screen reader?
  2. Document flow: Will the hidden element affect the document layout?
  3. Rendering: Will the hidden element’s box model be rendered?
  4. Event triggers: Does the element detect clicks or focus?

Now that we have our criteria out of the way, let’s compare the methods. Again, we’ll put everything together at the end in a way that we can use it as a reference for making decisions when hiding things in CSS.

Method 1: The display property

We kicked off this post with a caution about using display to hide content. And as we established, using it to hide an element means that the element is not generated at all. It’s in the DOM, but never actually rendered.

The element will still show in the markup, if you inspect the page you will be able to see the element. The box model will not generate nor appear on the page, which also applies to all its children.

And what’s more, if the element has any event listeners — say a click or hover — they won’t register at all. And as we’ve discussed already, all the content will be ignored by screen readers. Here, we have two visible buttons and one hidden with display: none. All three buttons have click events but only the two visible buttons will render and register the clicks.

Display is the only property that will affect image request firing. If an image tag (or parent element) has a display property set to none either through inline CSS or by selector, the image will be downloaded. On the other hand, if the image is applied with a background property, it won’t be downloaded.

This is the case because the parser hasn’t applied the CSS when an HTML document is parsed and it encounters an <img> tag. On the other hand, when we apply the image to an element with a background property, the image won’t be downloaded because the parser hasn’t applied the CSS where the image is called. This behavior is matched across all latest browsers. The only exception is IE 11, which will download images in both cases.

Metric Result
Is the hidden content read by a screen reader?
Will the hidden element affect the document layout?
Will the hidden element’s box model be rendered?
Does the element detect clicks or focus?

Method 2: The visibility property

If an element’s visibility property is set to hidden, then the element is “visually hidden.” Being “visually hidden” sounds a lot like what display: none does, but it’s incredibly different in that the element is generated and rendered, but invisible. This means that the element’s box model is present, giving it dimensions that continue to occupy space on the screen even though it doesn’t appear to be there.

Imagine you’re wearing an invisible cloak that makes you invisible to others, but you are still able to bump into things. You’re physically there, even if you’re invisible to the human eye.

But that’s where the differences between “visually hidden” and “not displayed” end. In fact, elements hidden with visibility and display behave the same in terms of accessibility and event triggers. Invisible elements are inaccessible to screen readers and won’t register events, as we see in the following demo that’s exactly the same as the last example, but merely swaps display: none with visibility: hidden.

Metric Result
Is the hidden content read by a screen reader?
Will the hidden element affect the document layout?
Will the hidden element’s box model be rendered?
Does the element detect clicks or focus?

Method 3: The opacity property

The opacity property only affects the visual aspect of the element. If we set an element’s opacity to zero, the element will be fully transparent. Again, it’s a lot like visibility: hidden where we’re draping an invisible cloak on the element where it’s invisible, but still physically present.

In other words, what we have is a hollow, transparent element that acts like any other element, only it’s invisible. Sounds a lot like the visibility method, right? The difference is that a fully transparent element is still accessible to a screen reader and can register events, like clicks, as we see in the following example.

Metric Result
Is the hidden content read by a screen reader?
Will the hidden element affect the document layout?
Will the hidden element’s box model be rendered?
Does the element detect clicks or focus?

Method 4: The position property

Pushing an element off-screen with absolute positioning is another way developers often hide things. Using top and left, we can push the element so far off the screen that there’s no way it will ever be seen. It’s like hiding the cookie jar outside of the house so the kids (or maybe you!) can’t find them.

“Absolute” is the key word here. If we set position to absolute, an element is taken out of the document flow which is a way of saying it no longer adheres to its natural position in the DOM. In other words, the page doesn’t reserve any space for it, which knocks the element out of order visually, positioning it to it’s nearest positioned element if there is one, or the document root if nothing else.

We take advantage of absolute positioning by taking the “hidden” element out of the document flow and offsetting it toward the top-left with values of -9999px.

.hidden {   position: absolute;   top: -9999px;   left: -9999px; }
Metric Effect
Is the hidden content read by a screen reader?
Will the hidden element affect the document layout?
Will the hidden element’s box model be rendered?
Does the element detect clicks or focus?

If the hidden element contains focusable content, the page will scroll to the element when it is in focus, creating a sudden jump.

Method 5: The “visually hidden” class

So far, the position method is the closest we’ve seen to an accessibility-friendly way to hide things in CSS. But the problem with focusable content causing sudden page jumps isn’t great. Another approach to accessible hiding combines absolute positioning, the clip property and hidden overflow. Scott O’Hara blogged it back in 2017.

.visually-hidden:not(:focus):not(:active) {   clip: rect(0 0 0 0);    clip-path: inset(50%);   height: 1px;   overflow: hidden;   position: absolute;   white-space: nowrap;    width: 1px; }

Let’s break that down.

We need to remove the element from the document flow. The best way to do this is by using position: absolute. This will remove the element, but we won’t push it off the screen.

.visually-hidden {   position: absolute; }

We can hide the element by setting the width and height property to zero. Unfortunately, that won’t work because some screen readers will ignore elements with zero width and height. What we can do is set it to the second-lowest value, 1px. That means the content will easily overflow the space, so we also need overflow: hidden to make sure it doesn’t visually spill over.

.visually-hidden {   height: 1px;   overflow: hidden;   position: absolute;   width: 1px; }

To hide that one-pixel square, we can use the CSS clipping property. It is perfect for this situation, as it doesn’t affect screen readers. The content is there but, again, is visually hidden. The thing to note is that clip was deprecated in favor of clip-path but is still needed if we need to support older versions of Internet Explorer.

.visually-hidden {   clip: rect(0 0 0 0);   clip-path: inset(50%);   height: 1px;   overflow: hidden;   position: absolute;   width: 1px; }

Another piece of the “visually hidden” class puzzle is to address smushed off-screen accessible text, an oddity that removes white-spacing between words, causing them to be read aloud like one big string of words. For example, “Welcome back home” will be read out as “Welcomebackhome.”

A simple solution to this problem is to set the white-space: nowrap:

.visually-hidden {   clip: rect(0 0 0 0);   clip-path: inset(50%);   height: 1px;   overflow: hidden;   position: absolute;   white-space: nowrap;   width: 1px; }

And, finally! The last thing to consider is to allow certain element with native focus and active sites to display when they are in focus, while continuing to prevent other elements, like paragraphs, from displaying. We can use the :not pseudo-selector for that.

.visually-hidden:not(:focus):not(:active) {   clip: rect(0 0 0 0);   clip-path: inset(50%);   height: 1px;   overflow: hidden;   position: absolute;   white-space: nowrap;   width: 1px; }
Metric Result
Is the hidden content read by a screen reader?
Will the hidden element affect the document layout?
Will the hidden element’s box model be rendered?
Does the element detect clicks or focus?

Honorable mentions

There are even more methods than the five we’ve covered. For example, the text-indent property can push text off the screen like the position method:

.hidden {   text-indent: -9999em; }

Unfortunately, this approach doesn’t jive with RTL writing modes. That makes it less adaptable than other solutions we’ve covered.

Another method is using transform to scale or move the element out of the way. It works the same — visually only — like opacity.

.hidden {   transform: scale(0); }

Let’s put everything together!

We got to a solution that will visually hide content but still be accessible. Then, should you stop using display: none? No, this is still the best way to hide an element completely (visually and accessibly).

That said, It is worth mentioning that if you want to achieve an opposite result — hide something from the screen reader, the aria-hidden="true" attribute will hide the content from screen readers, but not visually.

With that, here is a complete table that compares all of the approaches. Use it to guide your decisions on how to hide content next time you find yourself in that situation.

Metric Display Visibility Opacity Position Accessible Way
Is the hidden content read by a screen reader?
Will the hidden element affect the document layout?
Will the hidden element’s box model be rendered?
Does the element detect clicks or focus?

The post Comparing Various Ways to Hide Things in CSS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , ,

Comparing Styling Methods in 2020

Over on Smashing, Adebiyi Adedotun Lukman covers all these styling methods. It’s in the context of Next.js, which is somewhat important as Next.js has some specific ways you work with these tools, is React and, thus, is a components-based architecture. But the styling methods talked about transcend Next.js, and can apply broadly to lots of websites.

Here are my hot takes on the whole table-of-contents of styling possibilities these days.

  • Regular CSS. If you can, do. No build tooling is refreshing. It will age well. The only thing I really miss without any other tooling is nesting media queries.
  • Sass. Sass has been around the block and is still a hugely popular CSS preprocessor. Sass is built into tons of other tools, so choosing Sass doesn’t always mean the same thing. A simple Sass integration might be as easy as a sass --watch src/style.scss dist/style.css npm script. Then, once you’ve come to terms with the fact that you have a build process, you can start concatenating files, minifying, busting cache, and all this stuff that you’re probably going to end up doing somehow anyway.
  • Less & Stylus. I’m surprised they aren’t more popular since they’ve always been Node and work great with the proliferation of Node-powered build processes. Not to mention they are nice feature-rich preprocessors. I have nothing against either, but Sass is more ubiquitous, more actively developed, and canonical Sass now works fine in Node-land,
  • PostCSS. I’m not compelled by PostCSS because I don’t love having to cobble together the processing features that I want. That also has the bad side effect of making the process of writing CSS different across projects. Plus, I don’t like the idea of preprocessing away modern features, some of which can’t really be preprocessed (e.g. custom properties cannot be preprocessed). But I did love Autoprefixer when we really needed that, which is based on PostCSS.
  • CSS Modules. If you’re working with components in any technology, CSS modules give you the ability to scope CSS to that component, which is an incredibly great idea. I like this approach wherever I can get it. Your module CSS can be Sass too, so we can get the best of both worlds there.
  • CSS-in-JS. Let’s be real, this means “CSS-in-React.” If you’re writing Vue, you’re writing styles the way Vue helps you do it. Same with Svelte. Same with Angular. React is the un-opinionated one, leaving you to choose between things like styled-components, styled-jsx, Emotion… there are a lot of them. I have projects in React and I just use Sass+CSS Modules and think that’s fine, but a lot of people like CSS-inJS approaches too. I get it. You get the scoping automatically and can do fancy stuff like incorporate props into styling decisions. Could be awesome for a design system.

If you want to hear some other hot takes on this spectrum, the Syntax FM fellas sounded off on this recently.

The post Comparing Styling Methods in 2020 appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , ,