Introduction
This document explains how to implement responsive diagonally distributed elements (RDDE) using JavaScript. To simplify things, JavaScript Detect Element Resize by Sebastian Decima is used to detect resizes and make the code fully responsive. If you code along, you will have a replica of Responsive Diagonally Distributed Elements v0.1 by Samuel Newhouse, created from the ground-up, with an understanding of how it works.
This implementation relies on setting class names on a dedicated container element. The classes used and the size of the container element determine how its inner elements will be distributed. The class names will set the rules for:
- The angle of distribution (diagonally down or up)
- The basis for alignment (left or right margin)
- Blocking or not blocking elements from crossing the basis margin.
More options could be added to increase sophistication, but these three offer a solid starting point for understanding some of the complicating factors to consider when trying to create RDDE.
The Basic Concept
The most basic situation to consider is aligning a set of elements on a downhill diagonal starting from the left margin.

The inner element centers are spaced equidistant horizontally and the center of each element is on the distribution diagonal. The distribution diagonal is more of a conceptual aid than something fully calculated. Only the horizontal starting and ending points of the diagonal, relative to the left margin, are needed to begin with. These are determined by setting the starting horizontal position to the horizontal center of the first element and the ending horizontal position to the horizontal center of the last element. This will create a smoother and more consistent diagonal alignment that simply going from corner to corner of the container element.
There is no need to worry about the height of the container element or the heights of the inner elements. Flexbox is used to distribute the elements vertically by using flex-direction: column and justify-content: space-between. This effectively automates the slope of the diagonal without having to do any additional JavaScript operations. However, it's important to note that having inner elements with varying heights will throw off the diagonal alignment. Leveraging flexbox to reduce JavaScript usage requires each element to be on it's own line due to flex-direction:column.
Vertical Positioning
Keep in mind that ALL vertical positioning is determined entirely by using flexbox. The vertical start and end of the distribution diagonal, as well as each inner element's vertical position, are all automatically handled by using a flexbox set to flex-direction: column and justify-content: space-between. The only thing that will need to be implemented in JavaScript is the horizontal positioning.
Horizontal Spacing
It's easy to say that each element's center should align on the distribution diagonal, but how is that done? One idea is to set a separate left margin for each inner element in a way that makes all the inner elements line up diagonally. This requires a separate calculation for each inner element based on several factors:
- Width of outer container element
- Horizontal size of the distribution diagonal (distance between first and last element centers)
- Number of inner elements
- Width of the individual element being aligned
Here is a diagram with the first three of those considerations put into relationship with each other.

- box.clientWidth = Width of the outer container element
- innerWidth = Horizontal size of the distribution diagonal
- numParts = Number of inner elements
box.clientWidth is necessary for calculating innerWidth. Despite the diagram's appearance, the inner elements are not automatically spaced apart so that the last element's center falls in its proper place. Instead, we have to calculate innerStart, innerEnd and innerWidth by doing the following:
// parts is an array of all the inner elements.
innerStart = parts[0].clientWidth / 2;
innerEnd = box.clientWidth - (parts[numParts - 1].clientWidth / 2);
innerWidth = innerEnd - innerStart;
With innerWidth and numParts known, it's possible to calculate the horizontal spacing between element centers. This is the splitWidth and can be calculated by dividing innerWidth by one less than numParts.
splitWidth = innerWidth / (numParts - 1);
Calculating the Margins
Now look at how to use splitWidth with individual element widths to set the left margins:

There are several things added here that need explanation:
firstOffset: It's exactly the same value as innerStart. That may seem pointless, but it's to make explicit a meaningful contrast between itself and lastOffset.
lastOffset: NOT the same value as innerEnd. innerEnd is the distance from the left margin to where the horizontal center of the last element will be. lastOffset is half the width of the last element while firstOffset is half the width of the first element. This becomes more important in future examples.
align-items="flex-start": This is critical. Since the outer container is a flex box, the default alignment is "stretch". Using that default alignment would prevent properly using left margins as a method for aligning items diagonally. By setting the alignment to flex-start, all the inner elements will line up on the left side of the outer container. Then, the left margin of every element can be set to push them away from the left side as needed.
Now on to the marginLeft calculations:
splitWidth * i: One splitWidth worth of distance needs to be added for each inner element, so multiplying by an index counter works well here.
+ firstOffset: Adding firstOffset pushes each use of splitWidth over so the splitWidth distribution starts where it's supposed to (center of the first element).
- parts[i].clientWidth / 2: This is where each individual element width is taken into account. Even if the elements are different widths, this maintains every element's center position on the distribution diagonal.
In summary, the equivalent JavaScript so far would be:
// box is the container element for the parts
box.style.display = "flex";
box.style.flexDirection = "column";
box.style.justifyContent = "space-between";
box.style.alignItems = "flex-start";
var parts = box.children;
var numParts = parts.length
var firstOffset = parts[0].clientWidth / 2;
var lastOffset = parts[numParts - 1].clientWidth / 2;
var innerStart = firstOffset;
var innerEnd = box.clientWidth - lastOffset;
var innerWidth = innerEnd - innerStart;
var splitWidth = innerWidth / (numParts - 1);
// It's safe to skip i=0
for (let i = 1; i < numParts; i++) {
let marginLeft = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
parts[i].style.marginLeft = marginLeft + "px";
}
Notice the loop starts at i = 1. It's unnecessary to calculate i = 0 because it will always result in a zero left margin. Setting a zero left margin is the same as not setting a left margin since align-items: flex-start is being used.
Blocking on Margin
One possible issue with this method is that elements can receive negative margins if the container element becomes small enough. This is especially a problem if some elements are much larger than others. It many cases, it's better for elements to stop at zero left margin rather than go past that into the negatives. Luckily, this is fixed with a simple addition to the for loop:
for (let i = 1; i < numParts; i++) {
let marginLeft = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
That solves the problem, but it might be good to include an option for turning this blocking off. Checking for the existence of a particular class on the container element would do the trick.
var bBlock = !box.classList.contains("rdde-no-block");
for (let i = 1; i < numParts; i++) {
let marginLeft = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
Since the default case will be to block on the margin, the optional case must be to explicitly not block.
Container Element Classes
The previous section introduced the class "rdde-no-block". As mentioned at the very beginning, RDDE uses classes on elements to determine what elements are RDDE elements and what the settings will be.
<div class="rdde">
<span>A</span>
<span>B</span>
<span>C</span>
</div>
The above code demonstrates the idea. Simply giving the outter div the rdde class will allow it to diagonally distribute the three inside spans. Here are the classes covered in this document:
rdde (required): | sets up an element as the container |
rdde-down (default): | align inner elements diagonally down |
rdde-up: | align inner elements diagonally up |
rdde-left-basis (default): | calculate inner elements from left and block on left margin |
rdde-right-basis: | calculate inner elements from right and block on right margin |
rdde-no-block: | turn off blocking so elements can cross basis margin |
rdde-down with rdde-left-basis is essentially the concept and code that has been covered so far, along with rdde-no-block. With these as a starting point, now might be a good time to implement the more general foundations for the code. Once that is done, the other options are fairly easy to add later.
General Framework and Setup
Now that the general concepts and goals have been established, it's time to start creating RDDE as its own JavaScript file to be included in HTML (this file will be responsive-diagonally-distributed-elements.js). javascript-detect-element-resize.js needs to load before responsive-diagonally-distributed-elements.js. In this document, they are simply included in order right before the closing tag for the body:
</footer>
<script src="scripts/javascript-detect-element-resize.js"></script>
<script src="scripts/responsive-diagonally-distributed-elements.js"></script>
</body>
Additionally, the RDDE code needs to wait for the DOM content to be loaded before doing anything. To ensure all of this is done correctly, the following code waits for DOM content to finish loading, checks for the existence of javascript-detect-element-resize.js, and provides an error message and a suggested link if something is wrong.
document.addEventListener("DOMContentLoaded", function () {
if (typeof window.addResizeListener !== "function" || typeof window.removeResizeListener !== "function") {
console.log("ERROR: javascript-detect-element-resize.js is required for diagonally-distributed-elements.js");
console.log("See: https://github.com/sdecima/javascript-detect-element-resize");
return;
}
});
javascript-detect-element-resize.js adds the functions addResizeListener and removeResizeListener to the window object. If those functions don't exist on the window object, javascript-detect-element-resize.js isn't loaded and RDDE won't work. After that consideration, it's time to see if any elements actually have the rdde class assigned to them:
document.addEventListener("DOMContentLoaded", function () {
if (typeof window.addResizeListener !== "function" || typeof window.removeResizeListener !== "function") {
console.log("ERROR: javascript-detect-element-resize.js is required for diagonally-distributed-elements.js");
console.log("See: https://github.com/sdecima/javascript-detect-element-resize");
return;
}
var boxes = document.getElementsByClassName("rdde");
if (boxes.length == 0) {
console.log("Note: No elements with rdde class.");
return;
}
for (let i = 0; i < boxes.length; i++)
addRddeBox(boxes[i]);
function addRddeBox(box) {
}
function checkClasses(box) {
}
function distributeElements() {
}
});
If there are no elements with the rdde class, log a note saying so and return. If there are any elements with the rdde class, then addRddeBox gets passed each of those elements individually to start setting them up. Each function will be covered separately in further sections, but here is a quick breakdown.
addRddeBox: Sets the general style that will apply to every RDDE element, calls checkClasses, and sets the callback necessary to make everything responsive.
checkClasses: Checks for class options and sets defaults if not already set; also sets class specific flexbox properties.
distributeElements: The function that actually performs element distribution. It's also the callback function passed to window.addResizeListener.
Function addRddeBox
function addRddeBox(box) {
box.style.display = "flex";
box.style.flexDirection = "column";
box.style.justifyContent = "space-between";
// If text wraps, this keeps it looking good
box.style.textAlign = "center"; // <--------
checkClasses(box);
window.addResizeListener(box, distributeElements);
}
The styles set here apply to every RDDE element regardless of other options. textAlign = "center" is a good method for maintaining an even looking distribution if text in an inner element wraps. When text wraps, the element's clientWidth won't necessarily match the apparent text width. Centering the text within its container element makes this be much less of a problem.
checkClasses will be covered in the next section.
window.addResizeListener is a function created by javascript-detect-element-resize.js. The first argument accepts an element (box) that it monitors for resize. The second argument is a function that it will call anytime it detects that box has been resized. When addResizeListener calls distributeElements, it will set its this value to the element being resized.
Function checkClasses
function checkClasses(box) {
var classes = box.classList;
// Set defaults if not set.
if (!classes.contains("rdde-left-basis") && !classes.contains("rdde-right-basis"))
box.classList.add("rdde-left-basis");
if (!classes.contains("rdde-down") && !classes.contains("rdde-up"))
box.classList.add("rdde-down");
if (classes.contains("rdde-left-basis"))
box.style.alignItems = "flex-start";
else
box.style.alignItems = "flex-end";
}
Any default settings not overridden or made explicit will be added. This also sets the proper flexbox alignItems for rdde-left-basis and rdde-right-basis. rdde-down and rdde-right-basis will be fully explained later. They can be safely placed in the code now without interfering with the default options.
Function distributeElements
At this point, only the code for the left basis down sloping diagonal will be included here. Later, the code for all the other option combinations will be explained.
function distributeElements() {
var box = this;
var parts = box.children;
// Last child will be a div added from javascript-detect-element-resize.js.
// It must be ignored for this to work properly.
var numParts = parts.length - 1; // <-----------
var firstOffset = parts[0].clientWidth / 2;
var lastOffset = parts[numParts - 1].clientWidth / 2;
var innerStart = firstOffset;
var innerEnd = box.clientWidth - lastOffset;
var innerWidth = innerEnd - innerStart;
var splitWidth = innerWidth / (numParts - 1);
var bBlock = !box.classList.contains("rdde-no-block");
if (box.classList.contains("rdde-down")) {
if (box.classList.contains("rdde-left-basis")) {
for (let i = 1; i < numParts; i++) {
let marginLeft = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
}
else { // <-- box.classList.contains("rdde-right-basis")
}
}
else { // <-- box.classList.contains("rdde-up")
if (box.classList.contains("rdde-left-basis")) {
}
else { // <-- box.classList.contains("rdde-right-basis")
}
}
}
Most of these lines of code have been covered before, but there are a few new twists:
var box = this;
this is set to the element being resized when distributeElements is called from javascript-detect-element-resize.js.
var numParts = parts.length - 1;
javascript-detect-element-resize.js adds a div to the end of the container element. It must be ignored.
The if statements are a bit messy. But essentially, there are four combinations of settings, each requiring a different method for calculating the margins.
This code is now ready to work for the default configuration if there is no padding on the container element. The next section will show how to modify the code to allow for padding.
Container Element Padding
It's fairly easy to account for an rdde element itself having padding by slightly modifying distributeElements:
function distributeElements() {
var box = this;
var parts = box.children;
// Last child will be a div added from javascript-detect-element-resize.js.
// It must be ignored for this to work properly.
var numParts = parts.length - 1; // <-----------
var boxCS = getComputedStyle(box);
var distributionWidth = // Distribution should occur inside of any padding...
box.clientWidth - parseFloat(boxCS.paddingLeft) - parseFloat(boxCS.paddingRight);
var firstOffset = parts[0].clientWidth / 2;
var lastOffset = parts[numParts - 1].clientWidth / 2;
var innerStart = firstOffset;
var innerEnd = distributionWidth - lastOffset;
var innerWidth = innerEnd - innerStart;
var splitWidth = innerWidth / (numParts - 1);
...
The computed style of the rdde element is placed in the variable boxCS. Then a new variable called distributionWidth is added to store the width between left and right padding. distributionWidth is then used to calculate an innerEnd suitable for the padding. Although the code for assigning splitWidth is unchanged, it's influenced by innerWidth which is influenced by innerEnd, meaning the change propagates correctly through the code.
From this point forward, keep in mind that this change properly handles RDDE padding. To avoid clutter, it's not explicitly shown in any diagrams, but imagining any padding to exist outside the diagrams is conceptually accurate.
rdde-down rdde-left-basis
This is what has already been covered as the basic starting point. It's the default to be used if no direction or basis is provided.

There's is nothing to add here in terms of code or explanation. It's here more for convenience so all four options are laid out in sequence for comparison.
rdde-down rdde-right-basis

The first thing to notice here is that align-items is set to flex-end. This allows the right margin to be used for distributing the inner elements. There is also a new variable called iMirror. This gets the mirror index of the current index for the parts array so splitWidth can be multiplied by the proper amount. lastOffset is also being used in this situation instead of firstOffset. Starting from the right margin requires this.
Incorporating this into the existing JavaScript code is done in the main if statement in distributeElements function:
if (box.classList.contains("rdde-down")) {
if (box.classList.contains("rdde-left-basis")) {
for (let i = 1; i < numParts; i++) {
let marginLeft = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
}
else { // box.classList.contains("rdde-right-basis")
for (let i = 0; i < numParts - 1; i++) {
let iMirror = numParts - i - 1;
let marginRight = splitWidth * iMirror + lastOffset - parts[i].clientWidth / 2;
if (bBlock)
marginRight = Math.max(0, marginRight);
parts[i].style.marginRight = marginRight + "px";
}
}
}
else { // box.classList.contains("rdde-up")
if (box.classList.contains("rdde-left-basis")) {
}
else { // box.classList.contains("rdde-right-basis")
}
}
There's another mirror (of a kind) here in addition to iMirror. The index counter for rdde-left-basis + rdde-down starts at 1 because the 0 index can be ignored. Similarly, the last index can be ignored in the case of rdde-right-basis + rdde-down. The last index is always going to result in a zero rightMargin, so there's no need to waste time calculating it and setting it since align-items="flex-end".
rdde-up rdde-left-basis

In this diagram, the first element (i=0) is on the right instead of the left. This points to an important philosophical choice to be made for uphill diagonals. It might be argued that the order of elements should be automatically reversed in the RDDE code since a long uphill diagonal will be naturally read from left to right. That means not reversing the order could lead to confusion in some cases as the bottom element will probably be looked at first instead of the top element.
However, automatically reversing the order could also lead to confusion. As the diagonal shrinks and gets closer to vertical alignment, the more natural it becomes to read from top to bottom instead of left to right. This leaves the natural reading order ambiguous in many cases, and completely unknown from the perspective of trying to develop for all possible scenarios. The decision for RDDE is to leave the vertical order intact as it sits in the HTML. It's up to the developer to place the elements in the order they see fit.
For the code, just keep filling out the if-else statements to match the diagrams:
if (box.classList.contains("rdde-down")) {
if (box.classList.contains("rdde-left-basis")) {
for (let i = 1; i < numParts; i++) {
let marginLeft = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
}
else { // box.classList.contains("rdde-right-basis")
for (let i = 0; i < numParts - 1; i++) {
let iMirror = numParts - i - 1;
let marginRight = splitWidth * iMirror + lastOffset - parts[i].clientWidth / 2;
if (bBlock)
marginRight = Math.max(0, marginRight);
parts[i].style.marginRight = marginRight + "px";
}
}
}
else { // box.classList.contains("rdde-up")
if (box.classList.contains("rdde-left-basis")) {
for (let i = 0; i < numParts - 1; i++) {
let iMirror = numParts - i - 1;
let marginLeft = splitWidth * iMirror + lastOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
}
else { // box.classList.contains("rdde-right-basis")
}
}
There's not much to say about this code other than it's identical to the last section, except marginLeft is being set instead of marginRight.
rdde-up rdde-right-basis

This is just like the default option except everything is set away from the right margin instead of the left margin.
var bBlock = !box.classList.contains("rdde-no-block");
if (box.classList.contains("rdde-down")) {
if (box.classList.contains("rdde-left-basis")) {
for (let i = 1; i < numParts; i++) {
let marginLeft = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
}
else { // box.classList.contains("rdde-right-basis")
for (let i = 0; i < numParts - 1; i++) {
let iMirror = numParts - i - 1;
let marginRight = splitWidth * iMirror + lastOffset - parts[i].clientWidth / 2;
if (bBlock)
marginRight = Math.max(0, marginRight);
parts[i].style.marginRight = marginRight + "px";
}
}
}
else { // box.classList.contains("rdde-up")
if (box.classList.contains("rdde-left-basis")) {
for (let i = 0; i < numParts - 1; i++) {
let iMirror = numParts - i - 1;
let marginLeft = splitWidth * iMirror + lastOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
}
else { // box.classList.contains("rdde-right-basis")
for (let i = 1; i < numParts; i++) {
let marginRight = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
if (bBlock)
marginRight = Math.max(0, marginRight);
parts[i].style.marginRight = marginRight + "px";
}
}
}
Completed JavaScript
At this point, all the code for v0.1 of responsive-diagonally-distributed-elements.js should be complete and look like this:
document.addEventListener("DOMContentLoaded", function () {
if (typeof window.addResizeListener !== "function" || typeof window.removeResizeListener !== "function") {
console.log("ERROR: javascript-detect-element-resize.js is required for diagonally-distributed-elements.js");
console.log("See: https://github.com/sdecima/javascript-detect-element-resize");
return;
}
var boxes = document.getElementsByClassName("rdde");
if (boxes.length == 0) {
console.log("Note: No elements with rdde class.");
return;
}
for (let i = 0; i < boxes.length; i++)
addRddeBox(boxes[i]);
function addRddeBox(box) {
box.style.display = "flex";
box.style.flexDirection = "column";
box.style.justifyContent = "space-between";
// If text wraps, this keeps the diagonal alignment looking good
box.style.textAlign = "center"; // <----------------------------
checkClasses(box);
window.addResizeListener(box, distributeElements);
}
function checkClasses(box) {
var classes = box.classList;
// Set defaults if not set.
if (!classes.contains("rdde-left-basis") && !classes.contains("rdde-right-basis"))
box.classList.add("rdde-left-basis");
if (!classes.contains("rdde-down") && !classes.contains("rdde-up"))
box.classList.add("rdde-down");
if (classes.contains("rdde-left-basis"))
box.style.alignItems = "flex-start";
else
box.style.alignItems = "flex-end";
}
function distributeElements() {
var box = this;
var parts = box.children;
// Last child will be a div added from javascript-detect-element-resize.js.
// It must be ignored for this to work properly.
var numParts = parts.length - 1; // <-----------
var boxCS = getComputedStyle(box);
var distributionWidth = // Distribution must occur inside of any padding...
box.clientWidth - parseFloat(boxCS.paddingLeft) - parseFloat(boxCS.paddingRight);
var firstOffset = parts[0].clientWidth / 2;
var lastOffset = parts[numParts - 1].clientWidth / 2;
var innerStart = firstOffset;
var innerEnd = distributionWidth - lastOffset;
var innerWidth = innerEnd - innerStart;
var splitWidth = innerWidth / (numParts - 1);
var bBlock = !box.classList.contains("rdde-no-block");
if (box.classList.contains("rdde-down")) {
if (box.classList.contains("rdde-left-basis")) {
for (let i = 1; i < numParts; i++) {
let marginLeft = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
}
else { // box.classList.contains("rdde-right-basis")
for (let i = 0; i < numParts - 1; i++) {
let iMirror = numParts - i - 1;
let marginRight = splitWidth * iMirror + lastOffset - parts[i].clientWidth / 2;
if (bBlock)
marginRight = Math.max(0, marginRight);
parts[i].style.marginRight = marginRight + "px";
}
}
}
else { // box.classList.contains("rdde-up")
if (box.classList.contains("rdde-left-basis")) {
for (let i = 0; i < numParts - 1; i++) {
let iMirror = numParts - i - 1;
let marginLeft = splitWidth * iMirror + lastOffset - parts[i].clientWidth / 2;
if (bBlock)
marginLeft = Math.max(0, marginLeft);
parts[i].style.marginLeft = marginLeft + "px";
}
}
else { // box.classList.contains("rdde-right-basis")
for (let i = 1; i < numParts; i++) {
let marginRight = splitWidth * i + firstOffset - parts[i].clientWidth / 2;
if (bBlock)
marginRight = Math.max(0, marginRight);
parts[i].style.marginRight = marginRight + "px";
}
}
}
}
});
Compare Blocking Cases
In this section, click anywhere in the container elements to shrink or expand each box. Notice how rdde-left-basis and rdde-right-basis differ. Padding is also used in these rdde containers to demonstrate it working.
rdde-down rdde-left-basis
rdde-down rdde-right-basis
rdde-up rdde-left-basis
rdde-up rdde-right-basis
Compare Non-Blocking Cases
When margin blocking is turned off using rdde-no-block, the basis margin becomes essentially irrelevant. Click inside each container element to see what happens.
rdde-down rdde-left-basis rdde-no-block
rdde-down rdde-right-basis rdde-no-block
rdde-up rdde-left-basis rdde-no-block
rdde-up rdde-right-basis rdde-no-block
Conclusion and Improvements
There are several ways to improve both the performance and capabilities of RDDE v0.1. For performance, rdde container element data could be stored to reduce DOM lookups. For capabilities, an option could be added to set all inner elements to the same height to ensure perfect diagonal alignment in the case of varying heights. At the time of writing this document, v0.1 is the only version. Visit the RDDE github page to check for newer versions or make contributions to the code.