JSFiddle - React, Tailwind, and code Playground

by frumbert

HTML

<link rel="stylesheet" href="https://unpkg.com/normalize.css">
<script src="https://fonts.googleapis.com/css?family=Nunito+Sans:200,300,400,600,700"></script>
<script src="https://fonts.googleapis.com/css?family=Montserrat:400,700"></script>
<script src="https://unpkg.com/leader-line"></script>
<script src="https://unpkg.com/plain-draggable"></script>
<script src="https://unpkg.com/anim-event"></script>
    	<main>
    		<section class="template split-10-90">
    			<div class="pane cover-image">
                    <img src="https://images.unsplash.com/photo-1509813685-e7f0e4eaf3ee?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=60">
                </div>
    			<div class="pane overflow light">
    				<div class="padding">
<h1>Connect the items</h1>
<p>Drag connections between these items.</p>
<div class="split-connector">
    <div>
        <h3>Flavours</h3>
        <div id="plugs" class="list-group boxes">
            <div class="list-group-item"><span>Grape</span></div>
            <div class="list-group-item"><span>Vanilla</span></div>
            <div class="list-group-item"><span>Chocolate, but not that horrible stuff that Cadbury make</span></div>
            <div class="list-group-item"><img src="https://images.unsplash.com/photo-1534308983496-4fabb1a015ee?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=60"></div>
            <div class="list-group-item"><span>Lemon</span></div>
            <div class="list-group-item"><span>Lime like you get in the packets of Starburst snakes</span></div>
        </div>
    </div>
    <div class="connectors">
        <div class="connector start"></div>
        <div class="connector end"></div>

        <div class="connector start"></div>
        <div class="connector end"></div>

        <div class="connector start"></div>
        <div class="connector end"></div>

        <div class="connector start"></div>
        <div class="connector end"></div>

        <div...

CSS

:root {
  --text-fg: #222;
  --text-size: 20px;
  --button-bg: #31b7f9;
  --button-bg-light: #a6dffc;
}

html {
  box-sizing: border-box;
  font-size: var(--text-size);
  font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Ubuntu, Roboto, Noto, "Segoe UI", Arial, sans-serif;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

* {
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
}

body,
html {
  margin: 0;
  padding: 0;
  border: none;
  height: 100%;
}

body {
  letter-spacing: .01rem;
  line-height: 1.8;
  font-size: 1rem;
  font-weight: 400;
  font-family: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  letter-spacing: .01rem;
  text-shadow: 1px 1px 1px rgba(0, 0, 0, .004);
  color: var(--text-fg);
  overflow: hidden;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: Montserrat, sans-serif;
  font-weight: 300;
  line-height: 1.2;
  margin: 0
}

h1 {
  font-size: 3rem;
  letter-spacing: .025rem
}

h2 {
  font-size: 2.5rem
}

h3 {
  font-size: 2rem
}

h4 {
  font-size: 1.75rem
}

h5 {
  font-size: 1.5rem
}

h6 {
  font-size: 1.25rem
}

p {
  font-size: 1rem
}

body,
main {
  width: 100vw;
  height: 100vh;
}

main {
  position: relative;
}

.template {
  background-color: white;
  height: 100%;
}

[class*='stack-'],
[class*="split-"] {
  display: flex;
  transition: flex .3s;
}

[class*='stack-'] {
  flex-direction: column;
}

/* note: treat flex values like its a percentage */

.stack-evenly>*,
.stack-50>*,
.split-evenly>*,
.split-50>* {
  flex: 50;
}

.stack-30-70>*:first-of-type,
.split-30-70>*:first-of-type {
  flex: 30;
}

.stack-30-70>*:last-of-type,
.split-30-70>*:last-of-type {
  flex: 70;
}

.stack-20-80>*:first-of-type,
.split-20-80>*:first-of-type {
  flex:...

JavaScript

const connectorsNode = document.querySelector(".connectors");
const scrollableNode = connectorsNode.closest(".overflow");

const connectors = connectorsNode.querySelectorAll(".connector.start");
const lineWrapper = document.getElementById("line-wrapper");

const plugs = document.getElementById("plugs");
const plugPosition = plugs.getBoundingClientRect().y - plugs.previousElementSibling.getBoundingClientRect().height; // looks wrong but we are looking for a negaitve offset

const sockets = document.getElementById("sockets");
const socketPosition = sockets.getBoundingClientRect().y - sockets.previousElementSibling.getBoundingClientRect().height; // looks wrong but we are looking for a negaitve offset

function remToPx(rem) {
    return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
}

scrollableNode.addEventListener('scroll', AnimEvent.add(wrapperPosition), false);

function wrapperPosition() {
    lineWrapper.style.transform = 'none';
    const rect = lineWrapper.getBoundingClientRect();
    const sx = scrollableNode.scrollLeft;// - pageXOffset;
    const sy = scrollableNode.scrollTop; // - pageYOffset;
    const x = pageXOffset;
    const y = pageYOffset;
    lineWrapper.style.transform = 'translate(' +
        (-(rect.left + x + sx)) + 'px, ' +
        (-(rect.top + y + sy)) + 'px)';
}

let ends = [], snaptargets = [];
Array.from(connectors).forEach(function(start, index) {

    const end = start.nextElementSibling;

    const plug = plugs.children[index];
    const socket = sockets.children[index];

    let position = plug.getBoundingClientRect();
    let midpoint = (position.y - plugPosition) + (position.height / 2);
    const calc = "calc(" + midpoint + "px - .5rem)";
    start.style.top = calc;
    end.style.top = calc;

    position = socket.getBoundingClientRect();
    midpoint = (socket.getBoundingClientRect().y - socketPosition) + (position.height / 2);

    snaptargets.push({
        x: connectorsNode.offsetWidth - remToPx(.75),
...