# Common JavaScript recipes for CSS developers and designers

When building a component or a webpage, you can get pretty far with just HTML and CSS but you'll often find yourself hitting a wall when you want to add *interactivity* or make things *dynamic*. Of course this is where JavaScript comes in.

But maybe it sounds too daunting to learn a whole programming language just to enhance your CSS. Turns out that most use cases can be reduced to just a handful of "recipes". In this post, I will show you some of those common recipes and how they can help you take your UIs to the next level.

This post is meant to be beginner friendly (including for designers who haven't used JS much), so you may already be familiar with a lot of the content in here. I encourage you to use the table of contents to skip to the parts that interest you the most.

<details>

<summary>Prerequisites</summary>

We're not going to learn *everything* about JavaScript, but it helps to start with some extremely basic terminology.
<br /><br />

 **Variables:** This one is simple if you've used CSS custom properties or Sass variables. A variable is where you store a value and give it a name. Use the `let` keyword to define a variable (you can also use `const` if the value doesn't need to change, but please never use the `var` keyword!). You can assign a value to a variable using `=`.
<br /><br />

 **Functions:** If you've used functions or mixins in Sass, it's a similar idea. Functions let you write reusable blocks of code that you can *call* from multiple places. You can identify them from the `function` keyword or the `=>` (arrow) symbol. Functions can optionally accept "arguments" (think of them like *input*), perform operations on those arguments, and optionally "return" a value (think of it like an *output*).
<br /><br />

**Arrays:** Similar to lists in Sass, arrays can contain one or more items (e.g. an array of numbers, or an array of buttons). What's really cool is that arrays provides lots of different ways to access those items and do things with them. For example, if there is an array called `numbers`, you can access the first number in it using `numbers[0]`, and you can *loop* over all numbers using `numbers.forEach`.
<br /><br />

**Objects:** An object is a special type of value which consists of *properties*. Each of these properties can be assigned any kind of value (including functions and arrays), and you can access these properties using the dot notation. For example, if there is an object `tea` which has a property `flavor`, you can access it using `tea.flavor`.
<br /><br />

This was an extremely quick overview of the terminology you'll need for understanding the recipes in this post. If you want a more detailed write-up, check out Dan Abramov's  [*"What is JavaScript Made Of?"*](https://overreacted.io/what-is-javascript-made-of/).
</details>

## Basics ("ingredients")

We'll start with some very simple recipes (you can even think of them as "ingredients" for the advanced recipes). You can try these in the browser console.

### Accessing DOM elements

DOM elements represent html elements such as `<button>` and `<div>`. The webpage itself is represented by `document`.

There are many ways to access a DOM element, but my favorite way is to use the built-in `querySelector` and `querySelectorAll` functions. Both of these functions accept a CSS selector as the argument, and can be called from `document` or even from another element. `querySelector` returns the first instance of the selector that it finds, whereas `querySelectorAll` returns a list of all instances of that selector.

In the example below, we find the first `<section>` element in the page and store it in a variable called `firstSection`. Then inside that section, we find all `<button>` elements with the class `.text-button` and store them in a variable called `textButtons`. Since this is an array, we can then access the individual buttons from it.

```js
let firstSection = document.querySelector('section');
let textButtons = firstSection.querySelectorAll('button.text-button');

let firstTextButtonInSection = textButtons[0];
```

There are also a couple special elements which you don't have to query for: `document.documentElement` will give you the `<html>` element, and `document.body` will give you the `<body>` element.

You can access the parent of an element (let's say the element is stored in a variable called `el`) using `el.parentElement` and you can access its first or second child using `el.children[0]` and `el.children[1]` respectively.

Lastly, you can create an empty DOM element such as `<div></div>` using `document.createElement('div')`.

### Modifying DOM elements

Now that we have access to the elements, we can make changes to certain aspects of it. For these examples, let's assume we have a DOM element in a variable called `el`.

#### Changing attributes

You can get the current attribute value using `getAttribute`, assign a new value using `setAttribute`, toggle a boolean value using `toggleAttribute`, and remove an attribute using `removeAttribute`. The following snippet shows how to toggle the value of `aria-hidden` using a  [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator). We can't use `toggleAttribute` in this case because  [`aria-hidden` is not a boolean attribute](https://hiddedevries.nl/en/blog/2022-01-12-boolean-attributes-in-html-and-aria-whats-the-difference).

```js
let oldVal = el.getAttribute('aria-hidden');
let newVal = oldVal === 'true' ? 'false' : 'true';
el.setAttribute(`aria-hidden`, newVal);
```

In many cases, you can actually access the attributes directly using dot notation. For example, you can make a checkbox use the indeterminate state (which is only possible using JavaScript):
```js
let el = document.querySelector('[type=checkbox]');
el.indeterminate = true;
```

However, with this method, the attribute names are camelCase instead of kebab-case.
```js
el.ariaHidden = 'true';
```

There is also a special way of accessing `data-*` attributes: using `dataset`. The following snippet sets `data-active="true"` on the element.
```js
el.dataset.active = 'true';
```

#### Changing styles and class names

Possibly the most common thing you would want to do is programmatically change the styles or class names.

The following snippet is the equivalent of setting `style="color: rebeccapurple; background-color: thistle"` on an element.
```js
el.style.color = 'rebeccapurple';
el.style.backgroundColor = 'thistle';
```

This is particularly useful when combined with CSS custom properties which you can manipulate with `getPropertyValue` and `setProperty`. The following snippet reads the old value of `--x`, converts it to a number and increments it, then sends the new value back the `--x`.
```js
let oldX = el.style.getPropertyValue('--x');
let newX = Number(oldX) + 1;
el.style.setProperty('--x', newX);
```

For accessing class names, we have two ways: `className` will give you a single value where all the class names on an element are separated by spaces, and `classList` will give you a list of those classes. I  prefer `classList` because it provides a way to easily add/remove/toggle classes.
```js
el.classList.add('new-class');
el.classList.remove('old-class');
el.classList.toggle('is-active');
el.classList.toggle('is-active', true); // force this class to be present
```

#### Changing inner content

For accessing the inner text of an element, you can use `textContent`.
```js
el.textContent = 'hi';
```

You can add a new element inside another element using `appendNode`. This is useful if you programmatically created an element using `document.createElement`.

```js
let newChildEl = document.createElement('div');
el.appendNode(newChildEl);
```

You can also replace the entire markup of an element using `innerHTML` but be careful with this as it can create a [security risk](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#security_considerations).

```js
el.innerHTML = '<div>hi</div>';
```

## Recipes

Now that we know how to manipulate the DOM, we can start doing more advanced stuff. You could try these recipes in the browser console, but you probably want to use a `<script>` tag or even a separate `.js` file. I've also included a few codepen examples showing practical uses for these recipes.

### Respond to clicks

Perhaps the most common use case: make things clickable. You may already be doing this today by adding `onclick` directly in your HTML. But there's a better, more maintainable approach that doesn't require you to manually add `onclick` to all your buttons.

Let's first define a function called `handleClick` that will be called on every click. For now I'm simply logging this action to the browser console, but you can do pretty much anything in here (e.g. change some styles) now that you've learned how to manipulate the DOM.
```js
function handleClick() {
  console.log('Clicked!');
}
```

Now let's find our button and add an "event listener" to it. Every DOM element has an `addEventListener` function which takes two arguments: the name of the event (in this case `'click'`) and the name of the function (in this case `handleClick`).
```js
let primaryButton = document.querySelector('button.primary-button');
primaryButton.addEventListener('click', handleClick);
```

That's it! In fact, you're not limited to just `'click'` events. You have the ability to add listeners to all kinds of events; check out the [list of event types on MDN](https://developer.mozilla.org/en-US/docs/Web/Events).

### Toggle and store state

CSS doesn't offer much in the way of state management, so in the past you might have found yourself reaching for hacks like using an [invisible checkbox](https://css-tricks.com/the-checkbox-hack/). Fortunately, it's quite easy to do using JavaScript.

One of my favorite places to store state is in a data attribute, which can be easily toggled using JavaScript and also easily styled in CSS.
```js
let button = document.querySelector('.toggle-button');
function toggleActive() {
  button.dataset.active = button.dataset.active !== "true" ? "true" : "false";
}
button.addEventListener('click', toggleActive);
```
```css
[data-active="true"] {
  border: 2px solid;
}
```

Check out this CodePen where I'm using multiple data attributes and nested functions to create button groups that have independent state yet still share the same code.

%[https://codepen.io/MrRoboto/pen/BawMqgO]

### Media queries

Media queries are not just a CSS thing. You can access them in JavaScript to build some really powerful UIs. To do that, you can use `window.matchMedia`. In the following snippet, we check if the user prefers reduced motion, access the boolean result using the `matches` property, and store the opposite of that result (using `!`) in a variable called `motionOk`.

```js
let motionOk = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
```

We can also add an event listener that will be called every time the media query returns a new value. The following snippet will print `true` or `false` in the browser console every time the query runs.

```js
window.matchMedia('(max-width: 500px)').addEventListener(e => {
  console.log(e.matches);
});
```

Here's a practical example showing how to change default state and colors based on user's OS theme.

%[https://codepen.io/MrRoboto/pen/gOGEwQP]

### Window resizing

You have the ability to add a listener on the browser `'resize'` event, which will be called for each pixel change on the browser's window dimensions. I don't want to go into too much depth here for two reasons:
 1. You can do a lot with just media queries (and soon container queries).
 2. This method can be a bit expensive, and it is recommended to use [`ResizeObserver`](https://web.dev/resize-observer/) instead, although that is a bit harder to use.

But it can still be useful in some cases, so here's the syntax.

```js
function doSomethingWithWindowSize() {
  console.log(window.innerHeight, window.innerWidth);
}
window.addEventListener('resize', doSomethingWithWindowSize);
```

And here's the syntax for `ResizeObserver` which lets you observe dimension changes on *any* element (not just the browser window). Notice how we need to create a resize observer separately, and then manually call `observe` on the element.

```js
let el = document.querySelector('div');
let elResizeObserver = new ResizeObserver(([{ contentRect }]) => {
  console.log(contentRect.height, contentRect.width);
});
elResizeObserver.observe(el);
```

### Detect scroll position

This is a fun and powerful one, as it opens up quite a few possibilities. Similar to the `'resize'` event, the browser has a `'scroll'` event that we can listen to. And we can get the current scroll position using `window.scrollX` and `window.scrollY`.

```js
window.addEventListener('scroll', handleScrollEvent);
function handleScrollEvent() {
  console.log(window.scrollX, window.scrollY);
}
```

You can also read an element's current scroll position by calling `getBoundingClientRect()` on it, which will give you access to a bunch of information about the size and position of the element. In this case, we care about the `top` and `bottom` properties (or `left` and `right` for horizontal scrolling). I like to do this in a helper function where I can cleanly destructure those properties. The following function also uses `window.innerHeight` to return true if either the element's `top` or `bottom` are in viewport.

```js
function isElementInViewport(el) {
  const { top, bottom } = el.getBoundingClientRect();
  return top <= window.innerHeight && bottom > 0;
}
```

Lastly, we can force the page (or even an element) to scroll to a certain position by calling `scrollTo`.

```js
window.scrollTo(0, 0);
```

Let's look at a practical example. In this codepen, I'm adding a scroll listener to achieve two things:
- show a "scroll to top" button only after the page is scrolled down a bit
- change the "active" color in the header to match the currently visible color

%[https://codepen.io/MrRoboto/pen/gOGELrg]

## Conclusion

That's all folks! I hope you enjoyed reading this article as much as I enjoyed writing it. These recipes were based on my observations over the last few years. If you feel there is a pattern not covered here that is common enough in your workflow, I'd love to hear about it.

