tutorial // Aug 19, 2022

How to Dynamically Position Elements in the DOM with JavaScript

How to use JavaScript to dynamically manipulate DOM elements relative to other DOM elements.

How to Dynamically Position Elements in the DOM with JavaScript

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Why?

At first glance, this may seem a bit silly. Why would we want to do this? Well, when you start to build more complex interfaces, though many UI patterns are best attempted through CSS first, sometimes, that makes things more complicated than necessary. When that's the case for your own app, it's good to know how to apply styles via JavaScript to handle changes in your UI to avoid messy or fragile CSS.

Setting up our test case

For this tutorial, we're going to work with a Joystick component. This is the UI half of the Joystick framework we just set up. This will allow us to build out a UI quickly using plain HTML, CSS, and JavaScript.

To start, in the app that was created for us when we ran joystick create app, open up the /ui/pages/index/index.js file. Once you've got it, replace the contents with the following:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

Here, we're replacing the existing example component that's mapped to the root route in our application http://localhost:2600/ (or just /) with a skeleton component that we can use to build out our test case.

Next, let's replace that <div></div> being returned by the render() method (this is the HTML that will be rendered or "drawn" on screen) with a list of "cards" that we'll dynamically position later with JavaScript:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div class="index">
        <ul class="cards">
          <li>
            <h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
            <p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
          </li>
          <li>
            <h2>Ab recusandae minima commodi sed pariatur.</h2>
            <p>Velit in voluptatum quia consequatur fuga et repellendus ut cupiditate. Repudiandae dignissimos dolores qui. Possimus nihil laboriosam enim dolorem vitae accusantium accusamus dolor. Tenetur fuga omnis et est accusantium dolores. Possimus vitae aliquid. Vitae commodi et autem vitae rerum.</p>
          </li>
          <li>
            <h2>Voluptatem ipsa sed illum numquam aliquam sint.</h2>
            <p>Suscipit quis error dolorum sed recusandae recusandae est. Et tenetur perferendis sequi itaque similique. Porro facere qui saepe alias. Qui itaque corporis explicabo itaque. Quibusdam vel expedita odio quaerat libero veniam praesentium minus.</p>
          </li>
          <li>
            <h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
            <p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
          </li>
          <li>
            <h2>Ab recusandae minima commodi sed pariatur.</h2>
            <p>Velit in voluptatum quia consequatur fuga et repellendus ut cupiditate. Repudiandae dignissimos dolores qui. Possimus nihil laboriosam enim dolorem vitae accusantium accusamus dolor. Tenetur fuga omnis et est accusantium dolores. Possimus vitae aliquid. Vitae commodi et autem vitae rerum.</p>
          </li>
          <li>
            <h2>Voluptatem ipsa sed illum numquam aliquam sint.</h2>
            <p>Suscipit quis error dolorum sed recusandae recusandae est. Et tenetur perferendis sequi itaque similique. Porro facere qui saepe alias. Qui itaque corporis explicabo itaque. Quibusdam vel expedita odio quaerat libero veniam praesentium minus.</p>
          </li>
          <li>
            <h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
            <p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
          </li>
          <li>
            <h2>Ab recusandae minima commodi sed pariatur.</h2>
            <p>Velit in voluptatum quia consequatur fuga et repellendus ut cupiditate. Repudiandae dignissimos dolores qui. Possimus nihil laboriosam enim dolorem vitae accusantium accusamus dolor. Tenetur fuga omnis et est accusantium dolores. Possimus vitae aliquid. Vitae commodi et autem vitae rerum.</p>
          </li>
          <li>
            <h2>Voluptatem ipsa sed illum numquam aliquam sint.</h2>
            <p>Suscipit quis error dolorum sed recusandae recusandae est. Et tenetur perferendis sequi itaque similique. Porro facere qui saepe alias. Qui itaque corporis explicabo itaque. Quibusdam vel expedita odio quaerat libero veniam praesentium minus.</p>
          </li>
          <li>
            <h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
            <p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
          </li>
          <li>
            <h2>Ab recusandae minima commodi sed pariatur.</h2>
            <p>Velit in voluptatum quia consequatur fuga et repellendus ut cupiditate. Repudiandae dignissimos dolores qui. Possimus nihil laboriosam enim dolorem vitae accusantium accusamus dolor. Tenetur fuga omnis et est accusantium dolores. Possimus vitae aliquid. Vitae commodi et autem vitae rerum.</p>
          </li>
          <li>
            <h2>Voluptatem ipsa sed illum numquam aliquam sint.</h2>
            <p>Suscipit quis error dolorum sed recusandae recusandae est. Et tenetur perferendis sequi itaque similique. Porro facere qui saepe alias. Qui itaque corporis explicabo itaque. Quibusdam vel expedita odio quaerat libero veniam praesentium minus.</p>
          </li>
        </ul>
      </div>
    `;
  },
});

export default Index;

Very simple. Here, we've added a class index to the existing <div></div> and inside, we've added a <ul></ul> (unordered list) with a class cards. Inside, we've added 12 <li></li> tags, each representing a "card" with some lorem ipsum content on it. Though the length is technically arbitrary, in order to make sense of what we'll implement below, it makes sense to have several items as opposed to 1-2 (feel free to play with the length, though, as our code will still work).

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  css: `
    .cards {
      opacity: 0;
      border-top: 1px solid #eee;
      border-bottom: 1px solid #eee;
      padding: 40px;
      overflow-x: scroll;
      display: flex;
    }

    .cards li {
      background: #fff;
      border: 1px solid #eee;
      box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
      padding: 30px;
      border-radius: 3px;
      list-style: none;
      width: 300px;
      min-width: 300px;
    }

    .cards li h2 {
      font-size: 28px;
      line-height: 36px;
      margin: 0;
    }

    .cards li p {
      font-size: 16px;
      line-height: 24px;
      color: #888;
    }

    .cards li:not(:last-child) {
      margin-right: 30px;
    }
  `,
  render: () => {
    return `
      <div class="index">
        <ul class="cards">
          <li>
            <h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
            <p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
          </li>
          ...
        </ul>
      </div>
    `;
  },
});

export default Index;

Just above our render method, we've added a property to our component css which, as you'd expect, allows us to add some CSS styling to our component. What these styles are achieving is to give us a horizontally scrolled list of "cards" that extend past the edge of the browser, like this:

NDDAF30LAXVZA2Gm/7dhmxKb0BUoXPUD4.0
How our list looks with CSS applied.

Now that we have our base styles and markup in the browser, next, we want to add the JavaScript necessary to dynamically shift the first card in the list to start at the middle of the page. Our goal is to mimic a design like the "what's new" list on the current Apple Store design:

To do it, next, we're going to wire up the JavaScript necessary as a method on our Joystick component.

Dynamically setting padding on page load

Before we handle the "on page load" part here, first, we need to write the JavaScript to select our list in the DOM, calculate the current center point of the window, and then set the left-side padding of our list. Here's how we do it:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    defaultListPadding: '20px',
  },
  methods: {
    handleSetListPadding: (component = {}) => {
      const list = component.DOMNode.querySelector('ul.cards');
      const windowCenterPoint = window.innerWidth / 2;
      
      if (list) {
        list.style.paddingLeft = windowCenterPoint >= 400 ? `${windowCenterPoint}px` : component.state.defaultListPadding;
        list.style.opacity = 1;
      }
    },
  },
  css: `...`,
  render: () => {
    return `
      <div class="index">
        <ul class="cards">
          <li>
            <h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
            <p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
          </li>
          ...
        </ul>
      </div>
    `;
  },
});

export default Index;

On a Joystick component, a "method" (defined as a method function on the methods property of our component's option) is a miscellaneous function on our component that can be called from anywhere in the component. Here, we've defined handleSetListPadding as a method so that we can call it when our component mounts on screen (more on this in a bit).

To start, we add an argument as component which is automatically handed to us by Joystick (the framework automatically assigns the last possible argument on a function to be the component instance—since we don't have any arguments, it defaults to the first slot). On that component instance object, we're given a DOMNode property which represents the rendered DOM node for our component (in this case the Index component we're authoring) in the browser.

From that, we can use vanilla JavaScript DOM selection and here, we do that by using the .querySelector() method on that DOM node to locate our ul.cards list, storing it in a variable list.

Next, because we want to set that list's left-side padding to be the center of the window, we need to calculate what the pixel value of that center point is. To do it, we can take the window.innerWidth value and divide it by 2 (for example, if our window is currently 1000 pixels wide, windowCenterPoint would become 500).

With our list and windowCenterPoint assuming we did find a list element in the page, we want to modify the list.style.paddingLeft value, setting it equal to a string value, concatenating the value of windowCenterPoint with px (we do this because the value we get is an integer but we need to set our padding as a pixel value).

Notice that here, we make this paddingLeft value conditional based on the value of windowCenterPoint. If the value is greater than 400, we want to set it as the paddingLeft. If it's not, we want to fall back to a default padding value (this ensures we don't accidentally shove the cards completely off screen for smaller viewports). To store this default, we've added the state property to our component's options which is an object containing default values for the state of our component. Here, we've assigned defaultListPadding to a string '20px' which we use as the "else" in our windowCenterPoint >= 400 ternary.

Next, just beneath our call to set list.style.paddingLeft we also make sure to set list.style.opacity to 1. Why? Well, in our css that we set earlier, we set our list to opacity: 0; by default. This is a "trick" to prevent our list from jumping visually on page during a slow page load (hit or miss depending on connection speed). This removes any potential for a visual glitch which would be jarring to the user.

While we've got our code written, this currently won't do anything. To make it work, we need to actually call our method.

Calling handleSetListPadding on mount and window resize

This part is pretty simple, here's the code to get it done:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  state: {
    defaultListPadding: '20px',
  },
  lifecycle: {
    onMount: (component = {}) => {
      component.methods.handleSetListPadding();

      window.addEventListener('resize', () => {
        component.methods.handleSetListPadding();
      });
    },
  },
  methods: {
    handleSetListPadding: (component = {}) => {
      const list = component.DOMNode.querySelector('ul.cards');
      const windowCenterPoint = window.innerWidth / 2;
      
      if (list) {
        list.style.paddingLeft = windowCenterPoint >= 400 ? `${windowCenterPoint}px` : component.state.defaultListPadding;
        list.style.opacity = 1;
      }
    },
  },
  css: `...`,
  render: () => {
    return `
      <div class="index">
        <ul class="cards">
          <li>
            <h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
            <p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
          </li>
          ...
        </ul>
      </div>
    `;
  },
});

export default Index;

Adding one more option to our component lifecycle, on the object passed to it we assign a property onMount which is set to a function Joystick will call as soon as our component's HTML is rendered to the browser. Just like with our handleSetListPadding method, Joystick automatically passes the component instance to all of the available lifecycle methods.

Here, we use that component instance to access our handleSetListPadding method, calling it with component.methods.handleSetListPadding(). In addition to this, we need to also consider the user resizing the browser and how this will affect the window's center point. All we need to do is add an event listener on the window for the resize event and in the callback that's called when that event is detected, another call to component.methods.handleSetListPadding().

This works because we're retrieving the value of window.innerWidth at call time for the handleSetListPadding function. Here, then, because we're getting that value after the resize has occurred, we can trust that window.innerWidth will contain the current width and not the width that we had on page load.

That's it! Now if we load up our page in the browser, we should be able to resize and see our first card shift its left edge to align to the center of the window.

Wrapping up

In this tutorial, we learned how to manipulate the DOM dynamically with JavaScript. We learned how to dynamically position an element via its CSS using the DOM style property on a list element. We also learned how to rely on the window resize event to recalculate our browser's center point whenever the browser width changed.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode