JSFiddle - React, Tailwind, and code Playground
by HDL52
HTML
<div class="tag-scrollers">
<div class="tag-scroller">
<ul class="tag-list">
<li>Git</li>
<li>GitHub</li>
<li>React</li>
<li>Redux Toolkit</li>
<li>Redis</li>
<li>R3F</li>
<li>Docker</li>
<li>GraphQL</li>
<li>Postman</li>
<li>Unity</li>
<li>Playwright</li>
<li>Firebase</li>
<li>Vite</li>
<li>Vercel</li>
<li>MySQL</li>
<li>MongoDB</li>
<li>Material UI</li>
<li>NextUI</li>
<li>Ant Design</li>
<li>JavaScript</li>
<li>TypeScript</li>
<li>Python</li>
<li>Java</li>
<li>Ruby</li>
<li>Next.js</li>
<li>Vue.js</li>
<li>Express</li>
<li>ASP.NET</li>
</ul>
</div>
</div>
CSS
@import url('https://fonts.googleapis.com/css2?family=Concert+One&display=swap');
:root {
--dark: #252A2E;
--light: #F5F5F5;
--background: var(--light);
--text: var(--dark);
}
@media (prefers-color-schema: dark) {
:root {
--background: var(--dark);
--text: var(--light);
}
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
display: grid;
min-block-size: 100vh;
place-content: center;
background: var(--background);
color: var(--text);
}
.tag-scrollers {
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 100%;
max-width: 1200px;
overflow: hidden;
margin: auto;
}
.tag-scroller {
display: grid;
gap: 1.5rem;
mask: linear-gradient(90deg, #0000, var(--background) 15%, var(--background) 85%, #0000);
}
.tag-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
@media(prefers-reduced-motion) {
.tag-list {
flex-flow: row nowrap;
overflow: auto;
scrollbar-width: none;
scrollbar-color: transparent transparent;
}
.tag-list::-webkit-scrollbar-track {
background: transparent;
}
.tag-list::-webkit-scrollbar-thumb {
background: transparent;
}
.tag-list::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.tag-list li {
font-family: "Concert One";
font-size: 1rem;
line-height: 1;
padding: 0.625rem 1.375rem;
white-space: nowrap;
}
.tag-scroller.scrolling .tag-list {
width: max-content;
flex-wrap: nowrap;
animation: horizontal-scroll var(--duration) var(--direction, normal) linear infinite;
}
.tag-scroller.scrolling .tag-list:nth-child(even) {
--direction: reverse;
}
.tag-scroller:hover .tag-list {
/* animation-play-state: paused; */
}
@keyframes horizontal-scroll {
to {
/* 0.75rem FOR HALF THE GAP
OF THE .tag-scroller
*/
transform: translateX(calc(-50% - .75rem));
}
}
JavaScript
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
if (mediaQuery && !mediaQuery.matches) {
const tagScroller = document.querySelector(".tag-scroller");
const allTags = tagScroller.querySelectorAll("li");
const createElement = (tagName, className = "") => {
const elem = document.createElement(tagName);
elem.className = className;
return elem;
}
const scrollersFrom = (elements, numColumns = 2) => {
const fragment = new DocumentFragment();
elements.forEach((element, i) => {
const column = i % numColumns;
const children = fragment.children;
if (!children[column]) fragment.appendChild(createElement("ul", "tag-list"));
children[column].appendChild(element);
});
return fragment;
}
const addScrolling = () => {
tagScroller.classList.add("scrolling");
document.querySelectorAll(".tag-list").forEach((tagList) => {
const scrollContent = Array.from(tagList.children);
scrollContent.forEach((listItem) => {
const clonedItem = listItem.cloneNode(true);
clonedItem.setAttribute("aria-hidden", true);
tagList.appendChild(clonedItem);
});
tagList.style.setProperty("--duration", (tagList.clientWidth / 100) + "s");
});
}
const scrollers = scrollersFrom(allTags, 1);
tagScroller.innerHTML = "";
tagScroller.appendChild(scrollers);
addScrolling();
}