Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using it's URL and the proper URL extention.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <h1>"Re-orderable List" pattern</h1>

<p>My attempt to make a re-orderable list module that functions across platforms and devices while also being as accessible as possible for all users.</p>

<section>
	<h2 id="heading">Fruit Rankings</h2>	
	<p>Rank the following fruits from best-to-worst. Use an item's up and down buttons to reorder. You can also drag-and-drop an item onto another to swap positions, or drop an item above or below another to shift them.</p>
	<p>To reorder via text input, visit our <a href="#">classic interface</a>.</p>
	<p><span class="bold">Keyboard instructions:</span> use the up and down arrow keys to navigate list items; use the left and right arrow keys to access an item's up and down buttons.</p>
	
	<form>
	
		<div class="rankings">
			<a href="#rankingsSkipEnd" class="skipLink" id="rankingsSkipStart">&darr; Jump to list end</a>
			<ol>
				<li aria-labelledby="marker1 name1" class="rankingsItemLowfi">
					<div id="marker1" class="rankingsItem--marker">1.</div>
					<div class="rankingsItem--inner">
						<div id="name1" class="rankingsItem--text">Apple</div>
						<img class="rankingsItem--photo" src="https://assets.codepen.io/128542/apple.png" width="72" height="72" alt="" />
						<div class="rankingsItem--moveGroup">
							<label for="move1">Move <span class="visuallyHidden">Apple </span> to:</label>
							<input type="text" id="move1" inputmode="numeric" pattern="[0-9]*" autocomplete="off" />
						</div>
					</div>
				</li>
				<li aria-labelledby="marker2 name2" class="rankingsItemLowfi">
					<div id="marker2" class="rankingsItem--marker">2.</div>
					<div class="rankingsItem--inner">
						<div id="name2" class="rankingsItem--text">Banana</div>
						<img class="rankingsItem--photo" src="https://assets.codepen.io/128542/banana.png" width="96" height="auto" alt="" />
						<div class="rankingsItem--moveGroup">
							<label for="move2">Move <span class="visuallyHidden">Banana </span> to:</label>
							<input type="text" id="move2" inputmode="numeric" pattern="[0-9]*" autocomplete="off" />
						</div>
					</div>
				</li>
				<li aria-labelledby="marker3 name3" class="rankingsItemLowfi">
					<div id="marker3" class="rankingsItem--marker">3.</div>
					<div class="rankingsItem--inner">
						<div id="name3" class="rankingsItem--text">Cherry</div>
						<img class="rankingsItem--photo" src="https://assets.codepen.io/128542/cherry.png" width="96" height="auto" alt="" />
						<div class="rankingsItem--moveGroup">
							<label for="move3">Move <span class="visuallyHidden">Cherry </span> to:</label>
							<input type="text" id="move3" inputmode="numeric" pattern="[0-9]*" autocomplete="off" />
						</div>
					</div>
				</li>
				<li aria-labelledby="marker4 name4" class="rankingsItemLowfi">
					<div id="marker4" class="rankingsItem--marker">4.</div>
					<div class="rankingsItem--inner">
						<div id="name4" class="rankingsItem--text">Grape</div>
						<img class="rankingsItem--photo" src="https://assets.codepen.io/128542/grape.png" width="96" height="auto" alt="" />
						<div class="rankingsItem--moveGroup">
							<label for="move4">Move <span class="visuallyHidden">Grape </span> to:</label>
							<input type="text" id="move4" inputmode="numeric" pattern="[0-9]*" autocomplete="off" />
						</div>
					</div>
				</li>
				<li aria-labelledby="marker5 name5" class="rankingsItemLowfi">
					<div id="marker5" class="rankingsItem--marker">5.</div>
					<div class="rankingsItem--inner">
						<div id="name5" class="rankingsItem--text">Orange</div>
						<img class="rankingsItem--photo" src="https://assets.codepen.io/128542/orange.png" width="96" height="auto" alt="" />
						<div class="rankingsItem--moveGroup">
							<label for="move5">Move <span class="visuallyHidden">Orange </span> to:</label>
							<input type="text" id="move5" inputmode="numeric" pattern="[0-9]*" autocomplete="off" />
						</div>
					</div>
				</li>
			</ol>
			<a href="#rankingsSkipStart" class="skipLink" id="rankingsSkipEnd">&uarr; Jump to list start</a>

			<div class="buttonRowRight">
				<button id="updateBtn" class="button buttonSecondary" type="submit" name="update">Update List</button>
			</div>
		</div>
		
		<div class="buttonRowRight">
			<button id="saveBtn" class="button buttonLarge" type="submit" name="save">Save &amp; Continue</button>
		</div>
		
		<!-- 
		### adjusted markup should look like this ###
		
		<div class="rankings">
			<div role="application" aria-roledescription="Reorderable List widget" aria-labelledby="heading" aria-describedby="instructions">
				<div role="list">
					<div aria-labelledby="marker1 name1" role="listitem" class="rankingsItem">
						<div id="marker1" class="rankingsItem--marker">1.</div>
						<div class="rankingsItem--inner" draggable="true" data-name="Nikola Jokic" data-origin="1">
							<div id="name1" class="rankingsItem--text">
								<span>Nikola Jokic</span> 
								<span>Denver Nuggets</span>
							</div>
							<img class="rankingsItem--photo" src="https://assets.codepen.io/128542/jokic.png" width="96" height="auto" alt="" />
							<button class="rankingsItem--up" type="button">
								<span class="visuallyHidden">Move up</span>
								<svg width="16" height="16" focusable="false" aria-hidden="true">
									<use xlink:href="#icon--up" />
								</svg>                        
							</button>
							<button class="rankingsItem--down" type="button">
								<span class="visuallyHidden">Move down</span>
								<svg width="16" height="16" focusable="false" aria-hidden="true">
									<use xlink:href="#icon--down" />
								</svg> 
							</button>
						</div>
					</div>
				</div>
				<div class="rankingsGaps" aria-hidden="true" tabindex="-1"></div>
			</div>
			<div class="rankingsEnd visuallyHidden">End of Reorderable List widget.</div>
			<div class="rankingsStatus visuallyHidden" role="status" aria-live="assertive" aria-atomic="true"></div>
		</div>
		
		-->
		
	</form>
</section>

<svg xmlns="http://www.w3.org/2000/svg" class="visuallyHidden" focusable="false" aria-hidden="true">
	<defs>
		<symbol id="icon--up" viewBox="0 0 14 14">
			<path fill="none" stroke="currentColor" stroke-width="2" d="M.7 10.1L7 3.8l6.3 6.3"/>
		</symbol>
		<symbol id="icon--down" viewBox="0 0 14 14">
			<path fill="none" stroke="currentColor" stroke-width="2" d="M.7 3.8L7 10.1l6.3-6.3"/>
		</symbol>
	</defs>
</svg>
              
            
!

CSS

              
                /* mixins */
@mixin breakpoint($point) {
	@media (max-width: $point + "px") { @content ; }
}

/* vars */
:root {
	--color--black: #191919;

	--color--grey-90: #1C1D1F;
	--color--grey-80: #2D2E2F;
	--color--grey-70: #3D4551;
	--color--grey-60: #565C65;
	--color--grey-50: #71767A;
	--color--grey-40: #8D9297;
	--color--grey-30: #A9AEB1;
	--color--grey-20: #C6CACE;
	--color--grey-10: #DFE1E2;
	--color--grey-5: #EDEFF0;
	--color--grey-4: #F1F3F6;
	--color--grey-3: #F5F6F7;
	--color--grey-2: #F7F9FA;
	--color--grey-1: #FBFCFD;

	--color--teal-4: #EAFBFF;
	--color--teal-10: #C0EEF9;
	--color--teal-20: #9EDEEE;
	--color--teal-30: #84CDDF;
	--color--teal-40: #50ACC3;
	--color--teal-50: #0097BD;
	--color--teal-60: #006D88;
	--color--teal-70: #00576C;

	--color--violet-4: #fef2ff;
	--color--violet-50: #be32d0;
	--color--violet-60: #93348c;
	--color--violet-70: #711e6c;
}

body {
	//background: var(--color--grey-1);
	color: var(--color--black);
	font-family: 'Helvetica Neue', 'Helvetica', sans-serif;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	margin: 0;
	padding: 2em;
}

h1 {
	font-size: 2.5em; font-weight: 700;
	line-height: 1.25;
	margin: 0 0 .75em;
}

h2 {
	font-size: 2em; font-weight: 700;
	line-height: 1.25;
	margin: 0 0 .75em;
}

p {
	font-size: 1.125em;
	line-height: 1.5;
	margin: 0 0 1.5em;
}

a {
	color: var(--color--teal-60);
}

.bold {
	font-weight: bold;
}

section {
	margin: 0 auto 2em;
	max-width: 40em;
}

.rankings {
	--buttonGap: 1em;
	--dotColor: rgba(0,0,0,.6);
	--dotDistance: 1em;
	--dragBorderScale: .98;
	--dur: .25;
	--rowGap: 2em;
	--markerW: 2em;
	--markerGap: 2em;
	--gapIndicatorScaleX: .5;
	--gapIndicatorOverhang: 4em;

	border-bottom: solid 1px var(--color--grey-20);
	margin: calc(2 * var(--rowGap)) 0 var(--rowGap);
	padding: 0 0 var(--rowGap);
	position: relative;
	text-align: left;
}

.rankingsGap {
	//border: solid 1px orange; box-sizing: border-box;
	height: var(--rowGap);
	margin-left: calc(var(--markerW) + var(--markerGap));
	position: relative;
	width: calc(100% - (var(--markerW) + var(--markerGap)));

	&::before {
		background: var(--color--violet-50); background: none;
		border-top: dashed 8px var(--color--violet-50);
		box-sizing: border-box;
		content: "";
		height: .5em; height: 1px;
		opacity: 0;
		position: absolute; left: 50%; top: 50%;
		transform: translate(-50%,-50%) scaleX(var(--gapIndicatorScaleX));
		transition: 
			opacity .25s ease,
			transform .25s ease-out
		;
		width: calc(100% + (var(--gapIndicatorOverhang)*2));
	}

	&:first-of-type {
		position: absolute;
		top: calc(-1 * var(--rowGap));
	}
}

.rankingsItem, .rankingsItemLowfi {
	margin: 0;

	display: grid;
	align-items: center;
	grid-template-columns: var(--markerW) 1fr;
	column-gap: var(--markerGap);

	&:focus {
		outline: 0;

		.rankingsItem--inner {
			background-color: var(--color--teal-4);
			border-color: var(--color--teal-50);
			color: var(--color--teal-70);
			outline: 0;

			.rankingsItem--text span {color: var(--color--teal-60);}

			&::before {
				opacity: 1;
			}
		}
	}
	
	&:focus-within {
		.rankingsItem--lowfiInner {
			background-color: var(--color--teal-4);
			border-color: var(--color--teal-50);
			color: var(--color--teal-70);
			outline: 0;
			
			.rankingsItem--text span {color: var(--color--teal-60);}
			.rankingsItem--moveGroup {
				background: var(--color--teal-10);	
			}
		}
	}
}

.rankingsItemLowfi {
	margin: 0 0 var(--rowGap);
}

.rankingsItem--marker {
	font-size: 1.25em; font-weight: 700;
}

.rankingsItemLowfi .rankingsItem--inner {
	background: #FFF;
	border: solid 1px var(--color--grey-40);
	padding: 0em 0em 0em 5em;
	position: relative;
	
	display: grid;
	grid-template-columns: 1fr auto; 
	grid-auto-flow: column;
	align-items: center;
	column-gap: 1em;
	
	.rankingsItem--photo {
		left: .5em;
	}
}

.rankingsItem--inner {
	background-color: #FFF;
	background-image: 
		radial-gradient(3px 3px at var(--dotDistance) calc(50% - 9px), var(--dotColor) 50%, transparent 50%),
		radial-gradient(3px 3px at var(--dotDistance) calc(50% - 3px), var(--dotColor) 50%, transparent 50%),
		radial-gradient(3px 3px at var(--dotDistance) calc(50% + 3px), var(--dotColor) 50%, transparent 50%),
		radial-gradient(3px 3px at var(--dotDistance) calc(50% + 9px), var(--dotColor) 50%, transparent 50%),
		radial-gradient(3px 3px at calc(var(--dotDistance) + 6px) calc(50% - 9px), var(--dotColor) 50%, transparent 50%),
		radial-gradient(3px 3px at calc(var(--dotDistance) + 6px) calc(50% - 3px), var(--dotColor) 50%, transparent 50%),
		radial-gradient(3px 3px at calc(var(--dotDistance) + 6px) calc(50% + 3px), var(--dotColor) 50%, transparent 50%),
		radial-gradient(3px 3px at calc(var(--dotDistance) + 6px) calc(50% + 9px), var(--dotColor) 50%, transparent 50%),
	;
	border: solid 1px var(--color--grey-60);
	cursor: grab;
	padding: .5em 1em .5em 7.5em;
	position: relative;

	//display: grid;
	//grid-template-columns: 1fr auto; 
	//grid-auto-flow: column;
	//align-items: center;
	//grid-gap: 1em; gap: 1em;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 1em;

	// acts as focus border
	&::before {
		border: solid 4px var(--color--teal-50);
		content: "";
		height: 100%;
		opacity: 0;
		position: absolute; top: 50%; left: 50%;
		transform: translate(-50%,-50%);
		//transition: opacity .1s ease;
		width: 100%;
	}

	// acts as drop border
	&::after {
		border: dashed 8px var(--color--violet-50);
		content: "";
		height: 100%;
		opacity: 0;
		position: absolute; top: 50%; left: 50%;
		transform: translate(-50%,-50%) scale(var(--dragBorderScale));
		transition: 
			opacity .25s ease,
			transform .25s ease-out;
		;
		width: 100%;
		z-index: -1;
	}

	&:hover {
		background-color: var(--color--teal-4);
		border-color: var(--color--teal-50);
		color: var(--color--teal-70);
		//cursor: grab;

		.rankingsItem--text span {color: var(--color--teal-60);}
	}

	&:focus-within {
		background-color: var(--color--teal-4);
		border-color: var(--color--teal-50);
		color: var(--color--teal-70);

		.rankingsItem--text span {color: var(--color--teal-60);}
	}
}

.rankingsItem--buttons {
	margin-left: auto;
	
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	
	button {
		background: #FFF;
		border: solid 1px var(--color--grey-60);
		border-radius: 50%;
		color: var(--color--black);
		font-size: 1em;
		height: 3em;
		margin-left: var(--buttonGap);
		padding: 0;
		position: relative;
		width: 3em;

		display: flex; align-items: center; justify-content: center;

		svg {
			height: 1em;
			width: 1em;
		}

		&::before {
			border: solid 4px var(--color--teal-50);
			border-radius: 50%;
			content: "";
			height: 100%;
			opacity: 0;
			position: absolute; top: 50%; left: 50%;
			transform: translate(-50%,-50%);
			//transition: opacity .1s ease;
			width: 100%;
		}

		&:hover {
			background: var(--color--teal-4);
			border-color: var(--color--teal-50);
			color: var(--color--teal-70);
		}
		&:focus {
			background: var(--color--teal-4);
			border-color: var(--color--teal-50);
			color: var(--color--teal-70);
			outline: 0;

			&::before {
				opacity: 1;
			}
		}
		&::-moz-focus-inner {border: 0;}
	}
	button:first-of-type {margin-left: 0;}
}

.rankingsItem--moveGroup {
	background: var(--color--grey-3);
	box-sizing: border-box;
	border-left: solid 1px var(--color--grey-40);
	height: 100%;
	padding: 1em 1em;
	
	display: flex;
	align-items: center;
	
	label {
		font-size: .75em; font-weight: 700;
		line-height: 1;
		margin: 0 .5em 0 0;
	}
	
	input {
		appearance: none;
		background: #FFF;
		border: solid 1px var(--color--grey-40);
		border-radius: .25em;
		font-size: 1em;
		padding: .5em .25em;
		position: relative;
		text-align: center;
		width: 2em;
		
		&:focus {
			border-color: transparent;
			box-shadow: 0 0 0 4px var(--color--teal-50);
			outline: 0;
		}
	}
}

.rankingsItem--text {
	font-size: 1.25em; font-weight: 700;
	line-height: 1;
	margin-right: auto;
	pointer-events: none;
	overflow-wrap: break-word;
	word-break: break-word;
	hyphens: auto;

	span:nth-of-type(2) {
		display: block;
		color: var(--color--grey-50);
		font-size: .7em; font-weight: 400;
		margin-top: .4em;
	}
}

.rankingsItem--photo {
	display: block;
	height: auto;
	pointer-events: none;
	position: absolute; top: 50%; left: 2.5em;
	transform: translateY(-50%);
	width: 4em;
}

.__itemGrab {
	.rankingsItem--inner {
		cursor: grabbing;
	}
}

.__itemDrag {
	.rankingsItem--inner {
		//cursor: grabbing;
		opacity: .4;

		&::after {opacity: 0 !important;}
	}
}

.__itemDragover {
	.rankingsItem--inner {
		--dragBorderScale: 1;

		background-color: var(--color--violet-4);
		border-color: var(--color--violet-50);
		color: var(--color--violet-70);
		//cursor: grabbing;

		.rankingsItem--text span {color: var(--color--violet-60);}

		button {pointer-events: none;}

		&::after {
			opacity: 1;
		}
	}
}

.__gapDragover {
	--gapIndicatorScaleX: 1;

	&::before {
		opacity: 1;
	}
}

.__slideFront, .__slideBack {
	--abs: 1;
	--dis: 1;

	.rankingsItem--inner {
		animation-name: slide;
		animation-timing-function: cubic-bezier(.61,.38,.3,.95);
		animation-duration: calc((var(--dur) * var(--abs)) * 1s);
		//animation-duration: 3s;
		z-index: 1;
	}
}

.__slideFront {
	.rankingsItem--inner {
		z-index: 2;
	}
}

.__shake {
	--xDis: 5px;

	.rankingsItem--inner {
		animation: shake .4s linear;
	}
}

.__drop {
	--dropX: 0;
	--dropY: 0;

	.rankingsItem--inner {
		animation: drop .3s ease-out;
		z-index: 2;
		
		&::after {
			transition-duration: 0s;
		}
	}
}


/* button styles */
.button {
	background: var(--color--teal-50);
	border: solid 1px var(--color--teal-60);
	border-radius: .25em;
	box-sizing: border-box;
	color: #FFF;
	font-size: 1em;
	font-weight: 700;
	line-height: 1.25;
	padding: .5em 1em;
	text-align: center;
	transition: background .2s ease;
	
	&:hover {
		background: var(--color--teal-60);
	}
	
	&:focus {
		outline: solid 4px var(--color--teal-50);
		outline-offset: 4px;
	}
	
	&:active {
		background: var(--color--teal-70);
	}
}

.buttonSecondary {
	background: var(--color--teal-10);
	color: var(--color--teal-60);
	
	&:hover {
		background: var(--color--teal-20);
	}
	
	&:active {
		background: var(--color--teal-30);
	}
}

.buttonLarge {
	font-size: 1.25em;
}

.buttonRowRight {
	display: flex;
	justify-content: flex-end;
	column-gap: 2em;
}
/* end button styles */


/* skip link styles */
.skipLink {
	background: var(--color--teal-4);
	border: solid 4px var(--color--teal-50);
	color: var(--color--teal-60);
	font-size: 1.25em; font-weight: 700;
	opacity: 0;
	padding: .5em 1em;
	position: absolute;
	transform: translateX(-9999px);
	z-index: 1;
	
	&:hover {
		background: var(--color--teal-10);
		color: var(--color--teal-70);
	}
	
	&:focus {
		opacity: 1;
		outline: solid 4px #FFF;
		transform: translateX(0);
	}
}
/* end skip link styles */


@include breakpoint(768) {
	body {
		padding: 1.5em;
	}

	.rankings {
		--buttonGap: .5em;
		--dotDistance: .5em;
		--markerGap: 1em;
		--gapIndicatorOverhang: 1.5em;
	}
	.rankingsItem {}
	.rankingsItem--marker {
		font-size: 1em;
	}
	.rankingsItem--inner {
		gap: .5em;
		padding: .5em .5em .5em 4em;
	}
	.rankingsItem--buttons {
		button {
			//height: 2em;
			//width: 2em;
		}
	}
	.rankingsItem--text {
		font-size: 1em;
		span:last-of-type {
			font-size: .8em;
		}
	}
	.rankingsItem--photo {
		left: 1.5em;
		width: 2em;
	}
	
	#saveBtn {
		display: block;
		width: 100%;
	}
}


/* keyframes */
@keyframes slide {
	0% {
		transform: translateY(calc((var(--dis) * 100%) + (var(--rowGap) * var(--dis))));
	}
	100%	{
		transform: translateY(0);
	}
}

@keyframes shake {
	0% {
		transform: translateX(0);
		animation-timing-function: ease-out;
	}
	16%,48%,80% {
		transform: translateX(calc(-1 * var(--xDis)));
		animation-timing-function: ease
	}
	32%,64% {
		transform: translateX(var(--xDis));
		animation-timing-function: ease;
	}
	100%	{
		transform: translateX(0);
		animation-timing-function: ease-in;
	}
}

@keyframes drop {
	0% {
		//opacity: .4;
		transform: translate(calc(1px * var(--dropX)),calc(1px * var(--dropY)));
	}
	100%	{
		//opacity: 1;
		transform: translate(0,0);
	}
}




/* utilities */
.visuallyHidden {
	clip: rect(0 0 0 0); 
	clip-path: inset(50%);
	height: 1px;
	overflow: hidden;
	position: absolute;
	white-space: nowrap; 
	width: 1px;
}
              
            
!

JS

              
                let wrapper,oldList,items,buttons,listEnd,status,wrapperFrag;
let gapHeight,firstGap;
let curName,curItem,curButton,curDraggedEl;
let grabCoords;
let statusTimer;

function debounce(func,delay) {
	let inDebounce;
	return function() {
		const context = this;
		const args = arguments;
		clearTimeout(inDebounce);
		inDebounce = setTimeout(() => func.apply(context, args), delay);
	}
}

function itemDragStart(e) {
	//console.log("dragstart");

	// add drag styling
	e.target.parentElement.classList.add('__itemDrag');

	items.forEach((item) => {
		item.classList.remove('__itemDragover');
	});

	// set currently dragged item
	curDraggedEl = e.target;

	// set mouse coords of grab
	grabCoords.x = e.layerX;
	grabCoords.y = e.layerY;

	// set drag data
	e.dataTransfer.effectAllowed = 'move';
	e.dataTransfer.setData('text/plain', curItem);
	
	// set drag image
	//e.dataTransfer.setDragImage(curDraggedEl,grabCoords.x,grabCoords.y);

	// feedback message
	announceStatus(`${curName} being dragged.`);
}

function itemDragEnd(e) {
    //console.log('dragend');
    
    // remove drag styling
    e.target.parentElement.classList.remove('__itemDrag');
    e.target.parentElement.classList.remove('__itemGrab');

    items.forEach((item) => {
        item.classList.remove('__itemDragover');
    });
}

function itemDragOver(e) {
    if (e.preventDefault) e.preventDefault();

    return false;
}

function itemDragEnter(e) {
    //console.log("dragenter");

    // get position of item dragged over
    let pos = parseInt(e.target.parentElement.dataset.pos)+1;

    // check that item isn't being dragged over itself
    if(pos !== curItem+1) {
        // add drag over styling
        e.target.parentElement.classList.add('__itemDragover');

        // set drag data
        e.dataTransfer.dropEffect = 'move';

        // feedback message
        announceStatus(`Entered drag area for #${pos}, ${e.target.dataset.name}. Drop to swap positions.`);
    } else {
        // set drag data
        e.dataTransfer.dropEffect = 'none';
    }
}

function itemDragLeave(e) {
    //console.log("dragleave");

    // remove drag over styling
    e.target.parentElement.classList.remove('__itemDragover');

    // feedback message
    //announceStatus(`Left drag area for ${e.target.dataset.name}.`);
}

function itemDrop(e) {
    //console.log("drop");

    // stop browser redirect
    e.preventDefault();
    e.stopPropagation();

    // remove drag styling
    curDraggedEl.parentElement.classList.remove('__itemDrag');
    curDraggedEl.parentElement.classList.remove('__itemGrab');
    //curDraggedEl.parentElement.classList = 'rankingsItem';

    // check that item was dropped on a different one
    if(curDraggedEl !== e.target) {
        // get #'s of items we're swapping
        let pos1 = parseInt(e.dataTransfer.getData('text/plain'));
        let pos2 = parseInt(e.target.parentElement.dataset.pos);

        // update grab coordinates
        grabCoords.x = grabCoords.x - e.layerX;
        grabCoords.y = grabCoords.y - e.layerY;

        // swap items
        swapItems(pos1,pos2);

        // move focus to item
        items[pos2].focus();

        // do drop transition
        transitionDropItem(pos2,grabCoords.x,grabCoords.y);

        // do slide transition
        transitionSlideItem(pos1,pos2 - pos1,false,.35);

        // feedback message
        // item [pos1] moved to [pos2], item [pos2] moved to [pos1]
        announceStatus(`Items ${pos1+1} and ${pos2+1} swapped: ${curName} moved to #${pos2+1}.`);
    }
  
    // some browsers need this to prevent redirect
    return false;
}

function gapDragEnter(e) {
    //console.log("gapdragenter");

    // get position of gap
    let pos = parseInt(e.target.dataset.pos);
    
    // check if gap is above or below item being dragged
    if(curItem !== pos && curItem !== pos-1) {
        // show gap
        e.target.classList.add('__gapDragover');

        // set drag data
        e.dataTransfer.dropEffect = 'move';

        // setup feedback message
        let message = '';

        // if first gap
        if(pos == 0) {
            message += `Entered drag area for gap before #1. Drop to move item to #1 and shift other items down.`;

        // if gap is last
        } else if(pos == items.length) {
            message += `Entered drag area for gap after #${items.length}. Drop to move item to #${items.length} and shift other items up.`;

        // if gap is in the middle
        } else {
            message = `Entered drag area for gap between ${pos} and ${pos+1}. `;

            // if we're dragging item above where it originatetd
            if(pos < curItem) {
                message += `Drop to move item to #${pos+1} and shift other items down.`;

            // if we're dragging item below where it originated
            } else {
                message += `Drop to move item to #${pos} and shift other items up.`;
            }
        }
        // announce feedback message
        announceStatus(message);
    } else {
        // set drag data
        e.dataTransfer.dropEffect = 'none';
    }
}

function gapDragLeave(e) {
    //console.log("gapdragleave");

    // hide gap
    e.target.classList.remove('__gapDragover');
}

function gapDragOver(e) {
    if (e.preventDefault) e.preventDefault();

    return false;
}

function gapDrop(e) {
    // stop browser redirect
    e.preventDefault();
    e.stopPropagation();

    // remove drag styling on gap
    e.target.classList.remove('__gapDragover');

    // remove drag styling
    curDraggedEl.parentElement.classList.remove('__itemDrag');
    curDraggedEl.parentElement.classList.remove('__itemGrab');

    // get position of gap
    let pos = parseInt(e.target.dataset.pos);

    // get distance dragged item is moving
    let dis = pos-curItem;        

    // check if gap is above or below item being dragged
    if(curItem !== pos && curItem !== pos-1) {

        // update grab coordinates
        grabCoords.x = grabCoords.x - e.layerX;
        if(dis < 0) {
            grabCoords.y = gapHeight - e.layerY + grabCoords.y;
        } else {
            grabCoords.y = -items[curItem].getBoundingClientRect().height - e.layerY + grabCoords.y;
        }

        // shift items
        shiftItems(curItem,dis);
    }

    // some browsers need this to prevent redirect
    return false;
}

function transitionSlideItem(pos,dis,front=true,decay) {
    // vars
    let cls;
    // get item
    let item = items[pos];
    // get duration css var to use as setTimeout duration later
    let dur = parseFloat(getComputedStyle(item).getPropertyValue('--dur'))*1000 || 0;
    
    // var to keep track of rate of change
    let rate;

    // check if we're moving an item more than 1 row and if a decay number was passed in
    if(Math.abs(dis) > 1 && decay) {
        rate = 0;
        // loop through number of rows we're moving item and add a decay to the rate of change
        for(let i = 0; i < Math.abs(dis); i++) {
            rate += 1 - (i * decay);
        }
        // set duration to match rate
        dur *= rate;

    // if moving only one row
    } else {
        // set rate var to have no change
        rate = Math.abs(dis);
    }

    // determine if row should be in front or back and add relevant class
    if(front) {
        cls = '__slideFront';
    } else {
        cls = '__slideBack';
    }
    
    // set value of css vars so item knows which direction to move and how far
    item.style.setProperty('--dis',dis);
    item.style.setProperty('--abs',rate);

    // add class to kick off animationn
    item.classList.add(cls);

    // remove class after animation has finished so as to not affect future animations
    setTimeout(() => {
        item.classList.remove(cls);
    }, dur);
}

function transitionShakeItem(pos) {
    // get item
    let item = items[pos];
    
    // add class to kick off animation
    item.classList.add('__shake');

    // remove class after animation has finished
    setTimeout(() => {
        item.classList.remove('__shake');
    }, 400);
}

function transitionDropItem(pos,dropX=0,dropY=0) {
    // get item
    let item = items[pos];
    
    // update css vars so row will slide from dropped position
    item.style.setProperty('--dropX',-dropX);
    item.style.setProperty('--dropY',-dropY);
    
    // add class to kick off animation
    item.classList.add('__drop');
    
    // remove class after animation has finished
    setTimeout(() => {
        item.classList.remove('__drop');
    }, 300);
}

function moveItemUp(pos) {
    // convert pos to number so we can math
    pos = parseInt(pos);

    // check to make sure we're not in the first row
    if(pos > 0) {
        // get positions of items
        let pos1 = pos-1;
        let pos2 = pos;

        // get ref to up button
        let button = items[pos2].querySelector('.rankingsItem--up');

        // swap items
        swapItems(pos2,pos1);

        // move focus to down button
        button.focus();
        
        // do slide transitions
        transitionSlideItem(pos1,1);
        transitionSlideItem(pos2,-1,false);
        
        // feedback message
        // item [pos2] moved to [pos1], item [pos1] moved to [pos2]
        announceStatus(`${curName} moved up to #${pos2}.`);
    } else {        
        // get ref to up button
        let button = items[pos].querySelector('.rankingsItem--up');
        
        // move focus to up button
        button.focus();

        // do shake animation
        transitionShakeItem(pos);
        
        // feedback message
        // item 1 is the first item and can't be moved up
        announceStatus(`${curName} is #1 and can't move up.`);
    }
}

function moveItemDown(pos) {
    // convert pos to number so we can math
    pos = parseInt(pos);

    // check to make sure we're not in the last row
    if(pos < items.length-1) {
        // get positions of items
        let pos1 = pos;
        let pos2 = pos+1;

        // get ref to down button
        let button = items[pos1].querySelector('.rankingsItem--down');

        // swap items
        swapItems(pos1,pos2);

        // move focus to down button
        button.focus();
        
        // do slide transitions
        transitionSlideItem(pos1,1,false);
        transitionSlideItem(pos2,-1);
        
        // feedback message
        // item [pos1] moved to [pos2], item [pos2] moved to [pos1]
        announceStatus(`${curName} moved down to #${pos2+1}.`);
    } else {
        // get ref to down button
        let button = items[pos].querySelector('.rankingsItem--down');

        // move focus to down button
        button.focus();

        // do shake animation
        transitionShakeItem(pos);
        
        // feedback message
        // item [pos] is the last item and can't be moved down
        announceStatus(`${curName} is #${items.length} and can't move down.`);
    }
}

function shiftItems(pos,dis) {
    let inner,item1,item2,newPos,id,name,marker,message;

    // get temp copy of items
    let tempItems = Array.prototype.slice.call(items);

    // get inner of element being dragged
    inner = items[pos].querySelector('.rankingsItem--inner');

    // get original position of item we're moving
    id = inner.dataset.origin;

    // if we're moving an element up (+ shifting other elements down)
    if(dis < 0) {
        // get partial array containing only the items being shifted
        tempItems = tempItems.slice(pos+dis,pos+1);

        // move inner to new item
        item1 = tempItems[0];
        item1.appendChild(inner);
        
        // update label
        updateLabel(pos+dis,id);

        // loop through array of partial items to take inner from one and place it inside the next
        for(let i = 0; i<tempItems.length-1; i++) {

            // get references to items we'll be swapping inners of
            item1 = tempItems[i];
            item2 = tempItems[i+1];

            // get inner so we can move it down one row
            inner = item1.querySelector('.rankingsItem--inner');

            // get label id for moved item
            id = inner.querySelector('.rankingsItem--text').id.substr(4);

            // move inner to next item
            item2.appendChild(inner);

            // update label
            marker = parseInt(item1.dataset.pos)+1;
            updateLabel(marker,id);
        }

        // get new position of item that was dragged
        newPos = pos + dis;

        // feedback message
        message = `${curName} moved up to #${newPos+1}`;
        if(Math.abs(dis)==1) {
            message += `, item ${newPos+1} shifted down.`;
        } else {
            message += `, items ${newPos+1}-${newPos+Math.abs(dis)} shifted down.`;
        }
        announceStatus(message);

        // do drop transition on dragged item
        transitionDropItem(newPos,grabCoords.x,grabCoords.y);

        // loop through shifted items and slide transition them
        for(let i = newPos; i < newPos+Math.abs(dis); i++) {
            transitionSlideItem(i+1,-1,false);
        }

    // if we're moving an element down (+ shifting other elements up)
    } else {
        // get partial array containing only the items being shifted
        tempItems = tempItems.slice(pos,pos+dis);

        // move inner to new item
        item1 = tempItems[tempItems.length-1];
        item1.appendChild(inner);

        // update label
        updateLabel(pos+dis-1,id);

        // loop through partial array and take inner from one item and place it inside the next
        for(let i = tempItems.length-1; i>0; i--) {
            // get references to items we're using
            item1 = tempItems[i];
            item2 = tempItems[i-1];

            // get inner so we can move it up one row
            inner = item1.querySelector('.rankingsItem--inner');

            // get label id for next item
            id = inner.querySelector('.rankingsItem--text').id.substr(4);

            // move inner to next iteam
            item2.appendChild(inner);

            // update label
            marker = parseInt(item1.dataset.pos)-1;
            updateLabel(marker,id);
        }

        // get new position of item that was dragged
        newPos = pos + dis - 1;

        // feedback message
        message = `${curName} moved down to #${newPos+1}`;
        if(Math.abs(dis-1)==1) {
            message += `, item ${newPos+1} shifted up.`;
        } else {
            message += `, items ${((newPos+1) - (dis-1))+1}-${newPos+1} shifted up.`;
        }
        announceStatus(message);

        // do drop transition on dragged item
        transitionDropItem(newPos,grabCoords.x,grabCoords.y);

        // loop through changed rows and transition them
        for(let i = newPos-1; i > newPos-dis; i--) {
            transitionSlideItem(i,1,false);
        }
    }

    // move focus to dragged item
    items[newPos].focus();
}

function swapItems(pos1,pos2) {
    // get items in question
    let item1 = items[pos1];
    let item2 = items[pos2];

    // get inners we're swapping
    let inner1 = item1.querySelector('.rankingsItem--inner');
    let inner2 = item2.querySelector('.rankingsItem--inner');

    // update labels
    let id1 = inner1.dataset.origin;
    let id2 = inner2.dataset.origin;
    updateLabel(pos1,id2);
    updateLabel(pos2,id1);

    // move inners from one item to the other
    item1.appendChild(inner2);
    item2.appendChild(inner1);
}

function updateFocusInfo(pos) {
	// get name of item we're moving
	let name = items[pos].querySelector('.rankingsItem--inner').dataset.name;

	// check if prev item and new item are different (means new row was selected)
	if(curItem !== pos) {
		// get buttons within new item
		buttons = items[pos].querySelectorAll('button');

		// take prev item out of tab order and put new item in tab order
		items[curItem].tabIndex = -1;
		items[pos].tabIndex = 0;
	}

	// update current item vars
	curItem = parseInt(pos);
	curName = name;
	curButton = null;
}

function itemFocus(e) {
    updateFocusInfo(e.target.dataset.pos);
}

function buttonFocus(e) {
    // update focus info
    updateFocusInfo(e.target.parentElement.parentElement.parentElement.dataset.pos);

    // expose focused button to screen reader
    //e.target.setAttribute('aria-hidden','false');

    // update current button var
    curButton = e.target.dataset.pos;
}

function buttonBlur(e) {
    // hide button from screen reader
    //e.target.setAttribute('aria-hidden','true');
}

function itemKeyDown(e) {
    let keyCode = e.keyCode || e.which;

    switch (keyCode) {
        case 32:		// space
        case 13:		// enter
            break;
            
        case 27:        // esc
            e.preventDefault();
            //list.focus();
            listEnd.focus();
            break;

        case 37:		// left
            e.preventDefault();
            
            // check if curButton doesn't exist yet (meaning new item was selected)
            if(!curButton) curButton = buttons.length;

            // if we're not at first button
            if(curButton>0) {
                // update current button var and move focus to it
                curButton--;
                buttons[curButton].focus();

            // if we're at first button
            } else {
                // set focus to button's parent
                items[curItem].focus();
            }

            // move focus to prev button
            //getPrevButton(curButton).focus();
            break;
        case 38:		// up
            e.preventDefault();

            // move focus to prev item
            getPrevItem(curItem).focus();
            break;
            
        case 39:		// right
            e.preventDefault();

            // check if curButton doesn't exist yet (meaning new item was selected)
            if(!curButton) curButton = -1;

            // if we're not at last button
            if(curButton<buttons.length-1) {
                // update current button var and move focus to it
                curButton++;
                buttons[curButton].focus();

            // if we're at last button
            } else {
                // set focus to button's parent
                items[curItem].focus();
            }

            // move focus to next button
            //getNextButton(curButton).focus();
            break;
        case 40:		// down
            e.preventDefault();

            // move focus to next item
            getNextItem(curItem).focus();
            break;
            
        case 9:		// tab
            break;

        case 33:    // page up
            e.preventDefault();

            // move focus to first item
            items[0].focus();
            break;

        case 34:    // page down
            e.preventDefault();

            // move focus to last item
            items[items.length-1].focus();
            break;
            
        default:
            break;
    }
}

function getPrevItem(pos) {
    if(pos>0) {
        pos--;
    } else {
        pos = items.length-1;
    }
    return items[pos];
}

function getNextItem(pos) {
    if(pos<items.length-1) {
        pos++;
    } else {
        pos = 0;
    }
    return items[pos];
}

function getPrevButton(pos) {
    if(pos>0) {
        pos--;
    } else {
        pos = buttons.length-1;
    }
    return buttons[pos];
}

function getNextButton(pos) {
    if(pos<buttons.length-1) {
        pos++;
    } else {
        pos = 0;
    }
    return buttons[pos];
}

function announceStatus(message) {
    // clear any previous timers
    clearTimeout(statusTimer);

    // set text of status element
    status.innerText = message;

    // set up timer to clear status
    statusTimer = setTimeout(function(){ 
        status.innerText = '';
    }, 5000);
}

function updateLabel(pos,num) {
    items[pos].setAttribute('aria-labelledby',`marker${pos+1} name${num}`);
}

function setupItem(list,oldItem,pos) {
	// get elements
	let oldInner = oldItem.querySelector('.rankingsItem--inner');
	let oldImg = oldInner.querySelector('img.rankingsItem--photo');
	let oldText = oldInner.querySelector('.rankingsItem--text');
	let name = oldText.innerText;
	
	// set up new item
	let item = document.createElement('div');
	item.classList = "rankingsItem";
	item.setAttribute('aria-labelledby',`marker${pos+1} name${pos+1}`);
	item.setAttribute('role','listitem');
	item.dataset.pos = pos;
	item.tabIndex = -1;
	
	// set up marker
	let marker = document.createElement('div');
	marker.id = `marker${pos+1}`;
	marker.classList = "rankingsItem--marker";
	marker.innerText = `${pos+1}.`;

	// set up inner
	let inner = document.createElement('div');
	inner.classList = "rankingsItem--inner";
	inner.dataset.name = name;
	inner.dataset.origin = pos+1;
	inner.setAttribute('draggable','true');
	inner.addEventListener('dragstart',itemDragStart,false);
	inner.addEventListener('dragend',itemDragEnd,false);
	inner.addEventListener('dragenter', itemDragEnter, false);
	inner.addEventListener('dragleave', itemDragLeave, false);
	inner.addEventListener('dragover', itemDragOver, false);
	inner.addEventListener('drop', itemDrop, false);
	inner.addEventListener('mousedown', (e) => {
		e.target.parentElement.classList.add('__itemGrab');
	}, false);
	inner.addEventListener('mouseup', (e) => {
		e.target.parentElement.classList.remove('__itemGrab');
	}, false);

	/* This event fixes a bug in Android Chrome where draging non-focused element does not assign focus and will result in wrong element being moved */
	inner.addEventListener('pointerdown', (e) => {
		updateFocusInfo(e.target.parentElement.dataset.pos);
		e.target.focus();
	}, false);
	
	// set up name and image elements
	let text = oldText.cloneNode(true);
	let img = oldImg.cloneNode(true);
	
	// set up buttons
	let buttons = [];
	let buttonSpan,buttonSvg,buttonUse;
	let ns = 'http://www.w3.org/2000/svg';
	let ns2 = 'http://www.w3.org/1999/xlink';
	// button wrapper
	let buttonWrapper = document.createElement('div');
	buttonWrapper.classList = "rankingsItem--buttons";
	// up button
	let upButton = document.createElement('button');
	upButton.classList = "rankingsItem--up";
	buttonSpan = document.createElement('span');
	buttonSpan.classList = "visuallyHidden";
	buttonSpan.innerText = "Move up";
	buttonSvg = document.createElementNS(ns,'svg');
	buttonSvg.setAttributeNS(ns,'width','16');
	buttonSvg.setAttributeNS(ns,'height','16');
	buttonSvg.setAttributeNS(ns,'focusable','false');
	buttonSvg.setAttributeNS(ns,'aria-hidden','true');
	buttonUse = document.createElementNS(ns,'use');
	buttonUse.setAttributeNS(ns2,'xlink:href','#icon--up');
	buttonSvg.appendChild(buttonUse);
	upButton.appendChild(buttonSpan);
	upButton.appendChild(buttonSvg);
	buttons.push(upButton);
	// down button
	let downButton = document.createElement('button');
	downButton.classList = "rankingsItem--down";
	buttonSpan = document.createElement('span');
	buttonSpan.classList = "visuallyHidden";
	buttonSpan.innerText = "Move up";
	buttonSvg = document.createElementNS(ns,'svg');
	buttonSvg.setAttributeNS(ns,'width','16');
	buttonSvg.setAttributeNS(ns,'height','16');
	buttonSvg.setAttributeNS(ns,'focusable','false');
	buttonSvg.setAttributeNS(ns,'aria-hidden','true');
	buttonUse = document.createElementNS(ns,'use');
	buttonUse.setAttributeNS(ns2,'xlink:href','#icon--down');
	buttonSvg.appendChild(buttonUse);
	downButton.appendChild(buttonSpan);
	downButton.appendChild(buttonSvg);
	buttons.push(downButton);
	
	// add elements to inner
	buttonWrapper.appendChild(upButton);
	buttonWrapper.appendChild(downButton);
	inner.appendChild(text);
	inner.appendChild(img);
	inner.appendChild(buttonWrapper);
	
	// add elements to item
	item.appendChild(marker);
	item.appendChild(inner);

	//updateLabel(pos,pos+1);

	// loop through buttons in each inner and set common functionality
	for(let i = 0; i < buttons.length; i++) {
		buttons[i].type = "button";
		buttons[i].tabIndex = "-1";
		buttons[i].dataset.pos = i;

		// add event listener
		buttons[i].addEventListener('focus',buttonFocus);
		buttons[i].addEventListener('blur',buttonBlur);
	}

	// change label of move up/down buttons
	upButton.children[0].innerText = `Move up ${name}`;
	downButton.children[0].innerText = `Move down ${name}`;

	// add event listeners for move up/down buttons
	upButton.addEventListener('click', (e) => {
		moveItemUp(curItem);
	});
	upButton.addEventListener('pointerdown', (e) => {
		e.stopPropagation();
	});
	downButton.addEventListener('click', (e) => {
		moveItemDown(curItem);
	});
	downButton.addEventListener('pointerdown', (e) => {
		e.stopPropagation();
	});

	// add event listeners for item
	item.addEventListener('focus', itemFocus);
	item.addEventListener('keydown', itemKeyDown);
	
	// create gap below item
	let gap = setupGap(pos+1);
	
	// add item and gap to list
	list.appendChild(item);
	list.appendChild(gap);
}

function setupGap(pos) {
	let gap = document.createElement('div');
	
	// set gap props
	gap.classList.add('rankingsGap');
	gap.dataset.pos = pos;
	gap.setAttribute('aria-hidden','true');
	
	// set up gap event listeners
	gap.addEventListener('dragenter', gapDragEnter, false);
	gap.addEventListener('dragleave', gapDragLeave, false);
	gap.addEventListener('dragover', gapDragOver, false);
	gap.addEventListener('drop', gapDrop, false);
	
	return gap;
}

function setGapHeight() {
	// get gap height var from CSS
	let height = getComputedStyle(wrapper).getPropertyValue('--rowGap').trim();
	
	// get current base font size
	let fontSize = getComputedStyle(document.documentElement).getPropertyValue('font-size').substr(0,2);
	
	// determine actual height of gap
	gapHeight = parseInt(height.substr(0,height.length-2)) * parseInt(fontSize);
	
	//gapHeight = firstGap.getBoundingClientRect().height;
	//console.log(gapHeight);
}

function setup() {
	// get wrapper and list element
	wrapper = document.querySelector('.rankings');
	oldList = wrapper.querySelector('ol');
	
	// get gap height
	setGapHeight();
	
	// create document fragment to build new DOM structure
	wrapperFrag = document.createDocumentFragment();
	
	// create application element
	let app = document.createElement('div');
	app.setAttribute('role','application');
	app.setAttribute('aria-roledescription','Reorderable List widget');
	app.setAttribute('aria-labelledby','heading');
	//app.setAttribute('aria-describedby','instructions');
	wrapperFrag.appendChild(app);
	
	// create list element
	let list = document.createElement('div');
	list.setAttribute('role','list');
	app.appendChild(list);
	
	// set up gap above list
	firstGap = setupGap(0);
	list.appendChild(firstGap);

	// loop through old list items and set up new ones
	items = oldList.querySelectorAll('.rankingsItemLowfi');
	for(let i = 0; i < items.length; i++) {
		setupItem(list,items[i],i);
	}
	// get updated array of items
	items = list.querySelectorAll('.rankingsItem');

	// make first item focusable initially
	items[0].tabIndex = 0;

	// set up escape element
	listEnd = document.createElement('div');
	listEnd.classList = 'rankingsEnd visuallyHidden';
	listEnd.textContent = 'End of Reorderable List widget.';
	listEnd.tabIndex = -1;
	wrapperFrag.appendChild(listEnd);

	// set up live region for accouncements
	status = document.createElement('div');
	status.classList = 'rankingsStatus visuallyHidden';
	status.setAttribute('role','status');
	status.setAttribute('aria-live','assertive');
	status.setAttribute('aria-atomic','true');
	wrapperFrag.appendChild(status);

	// set initial vars
	curName = '';
	curItem = 0;
	curButton = null;
	grabCoords = {x: 0, y: 0};
	
	// add new rankings DOM to page
	wrapper.innerHTML = '';
	wrapper.appendChild(wrapperFrag);
	
	// set up resize handler
	/*window.addEventListener('resize', debounce(() => {
		setGapHeight();
	}, 1000));*/
}

document.addEventListener('DOMContentLoaded', () => {
	console.clear();
	setup();
});
              
            
!
999px

Console