Write Less JavaScript

How many lines of JavaScript code were used to create the following interface?

At Mystery Science, we recently released this feature to capture feedback after a teacher has taught a lesson. There are four possible screens or states:

  1. Prompt (did you teach?)
  2. Rating (stars)
  3. Comments (open-ended feedback)
  4. Done

Each state (except the last one) prompts and subsequently saves user input. The server knows I taught a lesson as soon as I click “yes.” The star rating and comments work the same way — all responses are submitted via XHR. How little JS code can we write to get this feature working?

The first trick to writing less JS is to find the right abstractions. Let’s explore the two key abstractions here. When a teacher submits a response, JS is advancing the interface’s state. This can be accomplished in any number of ways but at the end of the day some elements are being shown (display: inherit1) and others are being hidden (display: none). JS also has to send the response to the server, which in jQuery-terms eventually means a call to $.ajax().

Let’s wire this up:

// Let's assume this feature lives inside <div class="lesson-feedback">.
var container = $('.lesson-feedback');

// First, let's wire up the UI:
container.find('.no-control, .submit-comments-control, .skip-comments-control')

// Now, let's wire up the response submissions:

This is a great start! This code is readable and there’s not much of it. But we’ve introduced some event handlers like enterRatingState() that are not defined. Presumably, these functions will reach into the container and show and hide their respective elements. But given that each state has many elements, how can we do this without introducing a bunch of JS? Let’s introduce some CSS to do the heavy-lifting. This is the second trick to writing less JS: defer view logic to CSS whenever possible.

.lesson-feedback.state-rating .rating,
.lesson-feedback.state-rating .visible-during-rating {
  display: inherit;
.lesson-feedback.state-rating .prompt,
.lesson-feedback.state-rating .comments,
.lesson-feedback.state-rating .done,
.lesson-feedback.state-rating .hidden-during-rating {
  display: none;

Let’s unpack this. Obviously, .lesson-feedback is the container. We’ll modify the container with .state-* classes to enter and exit the four states. This means there will also be .state-prompt, .state-comments, and .state-done. Each state needs blocks like the ones above. I’ve also included .visible-during-* and .hidden-during-* helper classes to control elements that span states (like the header and image). It may seem like a lot of CSS but once it’s there you shouldn’t need to change it unless you add or remove a state. Some of the repetition can also be reduced with a preprocessor like LESS or SASS.

Now, enterRatingState() and the other state-related functions are really lightweight:

// You'll need a function like this for each state:
function enterRatingState() {

function enterState(state) {
  container.removeClass('state-prompt state-rating state-comments state-done');
  container.addClass('state-' + state);

That’s all we need to drive the interface; looking good! Now, let’s look at data submission. In the first code example, I introduced submitViaXHR(). Let’s define that now.

function submitViaXHR(event) {

  var form = $(event.target);

  $.ajax(form.attr('action'), {
    type: form.attr('method'),
    data: form.serialize()

Remember, all the forms have submitViaXHR() bound as an event handler on submit. Instead of submitting to the server with a traditional page refresh, jQuery steps in and submits the data via XHR instead — this is nothing new. The real elegance here is that they all submit the same way. There are not three different handlers for the prompt, rating, and comments forms. The third trick to writing less JS is to keep data in HTML. Here’s an example of the HTML for the first state, prompt:

<div class="lesson-feedback state-prompt">
  <div class="prompt">
    <div class="answer">
      <form action="/viewings" method="PUT" style="display: inline;">
        <input type="hidden" name="viewing[actually_taught]" value="true">
        <input type="submit" value="Yes" class="yes-control button button-green">

      <form action="/viewings" method="PUT" style="display: inline;">
        <input type="hidden" name="viewing[actually_taught]" value="false">
        <input type="submit" value="No" class="no-control button button-red">

  <!-- And so on for the other states... -->

There are two really lightweight forms with hidden values that submit to the server. That’s all we need. The destination of the request (/viewings), the HTTP verb (PUT), and the request data ({ viewing: { actually_taught: true} }) are all stored in the markup and that’s a good thing.

So let’s recap. To write less JavaScript:

  1. Find the right abstractions.
  2. Defer view logic to CSS whenever possible.
  3. Keep data in the HTML.

1: inherit is a better choice than block for visible elements. This approach will allow inline elements to preserve their inline display type.

Thanks for reading! I'm Avand.

I am a full-stack software engineer, product designer, and teacher. I’ve been working on the web for over a decade and am passionate about building great products.

I currently work at Airbnb, where I help internal product teams stay abreast with customer feedback. Before that, I was at Mystery Science, transforming how elementary school teachers teach science. And since 2013, I’ve worked on-and-off with General Assembly, teaching aspiring developers what I know about front-end web development.

I was born in Boston, grew up in Salt Lake City, and spent many years living in Chicago. Now, I call San Francisco my home.

I’m an aspiring rock climber. I have a love affair with music and cars, especially vintage BMWs and Volkswagens. One day, I’ll buy a van and transform it into an offroad-capable camping rig.

But that’s enough about me. How can I help you?

Read my other posts or get in touch: