Nov 28, 2023

Behavior wrappers

Something that’s been on my mind lately is the value of not encapsulating html, css, and javascript into singular components. Sometimes – if you value keeping things simple – teasing apart behavior and presentation into composable pieces makes more sense.

Enter custom elements. As it happens, they’re the perfect vehicle for creating encapsulated behavior that you can use in all sorts of different contexts, and have it Just Work™, with progressive enhancement as the default behavior.

Here’s a relevant example from a project I’m working on. This is a custom element called <jitter-bug>. It’s job? Make a mess. Here’s how you use it:

<jitter-bug jitter="20" scale="10" rotation="15">
  <p>this is</p>
  <p>completely arbitrary</p>
  <p>markup</p>
</jitter-bug>

Stick some markup inside, and twiddle with a few knobs to tune the effect. It’s not concerned at all with what’s inside in terms of markup or styling. It’s only job is to mess things up. This’d be hard/weird as a kitchen-sink component, but sticking to behavior-only keeps things simple:

export default class JitterBug extends HTMLElement {
  static get observedAttributes() {
    return ['scale', 'jitter', 'rotation'];
  }

  get scale() {
    return this.hasAttribute('scale') ? this.getAttribute('scale') : '0';
  }

  get jitter() {
    return this.hasAttribute('jitter') ? this.getAttribute('jitter') : '0';
  }

  get rotation() {
    return this.hasAttribute('rotation') ? this.getAttribute('rotation') : '0';
  }

  static random(min, max) {
    const minimum = Math.ceil(min);
    const maximum = Math.floor(max);
    return Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
  }

  makeItMessy() {
    this.targetElements.forEach((element) => {
      const el = element;
      const rS = this.constructor.random(this.scale / 2, this.scale * 1.5);
      const rX = this.constructor.random(this.jitter * -1, this.jitter);
      const rY = this.constructor.random((this.jitter / 2) * -1, this.jitter / 2);
      const rR = this.constructor.random(this.rotation * -1, this.rotation);

      if (this.jitter > 0) {
        el.style.translate = `${rX}rem ${rY}rem`;
      }

      if (this.scale > 0) {
        el.style.scale = `${rS}%`;
      }

      if (this.rotation > 0) {
        el.style.rotate = `${rR}deg`;
      }
    });
  }

  connectedCallback() {
    this.targetElements = this.querySelectorAll(':scope > *');
    this.makeItMessy();
  }
}

window.customElements.define('jitter-bug', JitterBug);

I use this same basic approach for all sorts of things. <details> augmentations, content overflows, animation triggers, etc. Little behavior wrappers to use any place, any time.

Read more notes