Jotter

Nearly 3 years ago, I made a little Progressive Web App (PWA) with a simple goal. I wanted a web-based version of Tyke. It’s the simplest Mac app ever: a text box that pops out of your menu bar so you can take quick notes.

A little textbox app popping out of my menu bar with the text "I bloody love this"
Tyke in action

I didn’t like that Tyke was native only and a rather small window too, so I created Jotter back in October 2019.

All it is, is a <textarea> that if JavaScript is available (not when, if), a little app kicks in and syncs the content of that <textarea> with local storage.

The jotter app in a browser window
Jotter in action

For those interested, this is the app.js in all its glory:

import Content from './components/content.js';

class App {
  constructor() {
    this.today = new Date();
    this.todayLabel = document.querySelector('[for="jotterDay"]');
    this.weekLabel = document.querySelector('[for="jotterWeek"]');
    this.contentInstance = new Content();
    this.themeToggle = document.querySelector('[data-element="theme-toggle"]');
    this.jotterInstances = document.querySelectorAll('[data-element="jotter"]');

    // Show the button now that the JS is ready
    this.themeToggle.removeAttribute('hidden');

    // Set todays date on day label
    this.todayLabel.innerText = `Day notes (${this.today.toLocaleDateString(
      navigator.languages ? navigator.languages[0] : 'en-GB',
      {
        weekday: 'long',
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      }
    )})`;

    // Set week number on week notes
    // Logic nicked from here: https://gist.github.com/IamSilviu/5899269
    if (this.weekLabel) {
      const firstOfJan = new Date(this.today.getFullYear(), 0, 1);
      this.weekLabel.innerText = `Week notes (Week ${
        Math.ceil(((this.today - firstOfJan) / 86400000 + firstOfJan.getDay() + 1) / 7) - 1
      })`;
    }

    // Run the initial content application and the listener
    this.applyContent();
    this.listen();

    // Set theme auto-apply and toggle apply
    this.applyTheme();
    this.themeToggle.addEventListener('click', () => {
      this.applyTheme(true);
    });

    // Prevent save shortcut
    document.addEventListener(
      'keydown',
      function (e) {
        if ((window.navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey) && e.keyCode == 83) {
          e.preventDefault();
        }
      },
      false
    );
  }

  // Listens for the jotter box content changes and applies to local storage accordingly
  listen() {
    this.jotterInstances.forEach((jotter) => {
      jotter.addEventListener('input', () => {
        this.contentInstance.save(jotter.getAttribute('data-id'), jotter.value);
      });
    });

    // If content changes in another tab, sync it up
    window.addEventListener('storage', (evt) => {
      const {isTrusted} = evt;

      if (!isTrusted) {
        return;
      }

      this.applyContent();
    });

    // Toggle focus attribute on body when jotter is focused or blurred
    this.jotterInstances.forEach((jotter) =>
      jotter.addEventListener('focus', () => document.body.setAttribute('data-state', 'focus'))
    );
    this.jotterInstances.forEach((jotter) =>
      jotter.addEventListener('blur', () => document.body.removeAttribute('data-state'))
    );
  }

  // Loads the content from the conten interface and applies it
  applyContent() {
    this.jotterInstances.forEach(
      (jotter) => (jotter.value = this.contentInstance.load(jotter.getAttribute('data-id')))
    );
  }

  // Apply the dark mode or light mode and optionally store in localStorage
  applyTheme(store = false) {
    let currentSetting = localStorage.getItem('user-color-scheme');

    // No storage found, so try to parse from CSS
    if (!currentSetting) {
      currentSetting = getComputedStyle(document.documentElement).getPropertyValue('--color-mode');

      if (currentSetting.length) {
        currentSetting = currentSetting.replace(/\"|'/g, '').trim();
        localStorage.setItem('user-color-scheme', currentSetting);
      }
    }

    // Store in localStorage if required
    if (store) {
      const reverseSetting = currentSetting === 'dark' ? 'light' : 'dark';

      localStorage.setItem('user-color-scheme', reverseSetting);

      // Apply the reversed setting to HTML element and toggle button
      document.documentElement.setAttribute('data-user-color-scheme', reverseSetting);
      this.themeToggle.innerText = reverseSetting === 'dark' ? 'Light mode' : 'Dark mode';
    } else {
      // Apply current setting setting to HTML element and toggle button
      document.documentElement.setAttribute('data-user-color-scheme', currentSetting);
      this.themeToggle.innerText = currentSetting === 'dark' ? 'Light mode' : 'Dark mode';
    }
  }
}

const appInstance = new App();

Good ol’ vanilla JavaScript with no build step. It runs like a dream and Just Works™.

I only added one “major feature” since then: the ability to have weekly and daily notes next to each other:

The same jotter app, but now it has two text boxes for week and day notes.
Weekly notes in action

This is the key point of this post, really. Because I kept the tech stack super simple and built with progressive enhancement at the forefront, I barely ever have to touch it any more. I shipped a little update today, a simple view with no visible labels, which is the first time I’ve touched it since 2021!

It’s also a good case study of keeping things simple and then maintaining that attitude. Jotter doesn’t ever need to do anything more complex than it already does. Hell, it would be nice to have some sort of storage sync, but I think until browsers sync local storage with your other devices, it’ll just stay local because I really don’t want to add a complex build step or back-end.

The best thing about Jotter is that I know that whenever I need to make some notes, it will be there—ready—and it’ll work, because if all else fails, it’s just a humble little <textarea>. I’d honestly say its the work I’m most proud of in my entire near 15 year career as a designer and developer because of this.

So, if you need a quick web-based notepad that’ll always be there for you: give Jotter a try today!


👋 Hello, I’m Andy and this is my little home on the web.

I’m the founder of Set Studio, a creative agency that specialises in building stunning websites that work for everyone and Piccalilli, a publication that will level you up as a front-end developer.


Back to blog