<div class="text">
<h1>Our Best Clients š«</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus corporis beatae dignissimos eum optio voluptas totam hic! Minus quae similique, atque at possimus voluptate maxime eum nostrum deserunt excepturi eaque!</p>
</div>
<div class="marquee" data-reversed="false">
<div class="marquee-item">
<img width="176" height="40" src="https://assets.codepen.io/162656/marquee-logo1.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="186" height="41" src="https://assets.codepen.io/162656/marquee-logo2.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="138" height="36" src="https://assets.codepen.io/162656/marquee-logo3.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="138" height="30" src="https://assets.codepen.io/162656/marquee-logo4.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="169" height="40" src="https://assets.codepen.io/162656/marquee-logo5.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="109" height="43" src="https://assets.codepen.io/162656/marquee-logo6.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="73" height="49" src="https://assets.codepen.io/162656/marquee-logo7.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="127" height="39" src="https://assets.codepen.io/162656/marquee-logo8.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="49" height="48" src="https://assets.codepen.io/162656/marquee-logo9.svg" alt="" />
</div>
<div class="marquee-item">
<img width="161" height="44" src="https://assets.codepen.io/162656/marquee-logo10.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="160" height="46" src="https://assets.codepen.io/162656/marquee-logo11.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="167" height="41" src="https://assets.codepen.io/162656/marquee-logo12.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="125" height="40" src="https://assets.codepen.io/162656/marquee-logo13.svg" alt="" />
</div>
<div class="marquee-item">
<img width="170" height="41" src="https://assets.codepen.io/162656/marquee-logo14.svg" alt="client logo">
</div>
</div>
<div class="marquee" data-reversed="true">
<div class="marquee-item">
<img width="138" height="36" src="https://assets.codepen.io/162656/marquee-logo3.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="49" height="48" src="https://assets.codepen.io/162656/marquee-logo9.svg" alt="" />
</div>
<div class="marquee-item">
<img width="138" height="30" src="https://assets.codepen.io/162656/marquee-logo4.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="138" height="30" src="https://assets.codepen.io/162656/marquee-logo12.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="109" height="43" src="https://assets.codepen.io/162656/marquee-logo6.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="169" height="40" src="https://assets.codepen.io/162656/marquee-logo5.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="160" height="46" src="https://assets.codepen.io/162656/marquee-logo11.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="127" height="39" src="https://assets.codepen.io/162656/marquee-logo8.svg" alt="" />
</div>
<div class="marquee-item">
<img width="73" height="49" src="https://assets.codepen.io/162656/marquee-logo7.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="125" height="40" src="https://assets.codepen.io/162656/marquee-logo13.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="170" height="41" src="https://assets.codepen.io/162656/marquee-logo14.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="161" height="44" src="https://assets.codepen.io/162656/marquee-logo10.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="176" height="40" src="https://assets.codepen.io/162656/marquee-logo1.svg" alt="client logo" />
</div>
<div class="marquee-item">
<img width="186" height="41" src="https://assets.codepen.io/162656/marquee-logo2.svg" alt="client logo">
</div>
</div>
<footer class="page-footer">
<span>made by </span>
<a href="https://georgemartsoukos.com/" target="_blank">
<img width="24" height="24" src="https://assets.codepen.io/162656/george-martsoukos-small-logo.svg" alt="George Martsoukos logo">
</a>
</footer>
/* BASIC STYLES
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");
* {
box-sizing: border-box;
}
img {
max-width: 100%;
height: auto;
}
body {
font-family: "DM Sans", sans-serif;
margin: 50px 0;
background: whitesmoke;
}
.text {
max-width: 1000px;
padding: 0 15px;
margin: 0 auto 50px;
font-size: 1.5rem;
}
/* MARQUEE STYLES
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
.marquee {
position: relative;
display: flex;
gap: 20px;
overflow: hidden;
}
.marquee + .marquee {
margin-top: 20px;
}
.marquee-item {
width: 150px;
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
flex-shrink: 0;
background: white;
border-radius: 10px;
}
@media (min-width: 700px) {
.marquee {
gap: 40px;
}
.marquee + .marquee {
margin-top: 40px;
}
.marquee-item {
width: 250px;
padding: 30px;
}
}
/* FOOTER STYLES
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
.page-footer {
position: fixed;
right: 0;
bottom: 50px;
display: flex;
align-items: center;
padding: 5px;
z-index: 1;
font-size: 14px;
background: white;
}
.page-footer a {
display: flex;
margin-left: 4px;
}
const marquees = document.querySelectorAll(".marquee");
const mm = gsap.matchMedia();
window.addEventListener("load", function () {
mm.add(
{
isMobile: "(max-width: 699px)",
isDesktop: "(min-width: 700px)"
},
(context) => {
const { isMobile, isDesktop } = context.conditions;
marquees.forEach((marquee) => {
const marqueeItems = marquee.querySelectorAll(".marquee-item");
const reversed =
marquee.getAttribute("data-reversed") === "true" ? true : false;
horizontalLoop(marqueeItems, {
repeat: -1,
paddingRight: isDesktop ? 40 : 20,
speed: isDesktop ? 0.5 : 0.25,
reversed
});
});
}
);
});
/*
This helper function makes a group of elements animate along the x-axis in a seamless, responsive loop.
Features:
- Uses xPercent so that even if the widths change (like if the window gets resized), it should still work in most cases.
- When each item animates to the left or right enough, it will loop back to the other side
- Optionally pass in a config object with values like "speed" (default: 1, which travels at roughly 100 pixels per second), paused (boolean), repeat, reversed, and paddingRight.
- The returned timeline will have the following methods added to it:
- next() - animates to the next element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc.
- previous() - animates to the previous element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc.
- toIndex() - pass in a zero-based index value of the element that it should animate to, and optionally pass in a vars object to control duration, easing, etc. Always goes in the shortest direction
- current() - returns the current index (if an animation is in-progress, it reflects the final index)
- times - an Array of the times on the timeline where each element hits the "starting" spot. There's also a label added accordingly, so "label1" is when the 2nd element reaches the start.
*/
function horizontalLoop(items, config) {
items = gsap.utils.toArray(items);
config = config || {};
let tl = gsap.timeline({
repeat: config.repeat,
paused: config.paused,
defaults: { ease: "none" },
onReverseComplete: () => tl.totalTime(tl.rawTime() + tl.duration() * 100)
}),
length = items.length,
startX = items[0].offsetLeft,
times = [],
widths = [],
xPercents = [],
curIndex = 0,
pixelsPerSecond = (config.speed || 1) * 100,
snap = config.snap === false ? (v) => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural
totalWidth,
curX,
distanceToStart,
distanceToLoop,
item,
i;
gsap.set(items, {
// convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster.
xPercent: (i, el) => {
let w = (widths[i] = parseFloat(gsap.getProperty(el, "width", "px")));
xPercents[i] = snap(
(parseFloat(gsap.getProperty(el, "x", "px")) / w) * 100 +
gsap.getProperty(el, "xPercent")
);
return xPercents[i];
}
});
gsap.set(items, { x: 0 });
totalWidth =
items[length - 1].offsetLeft +
(xPercents[length - 1] / 100) * widths[length - 1] -
startX +
items[length - 1].offsetWidth *
gsap.getProperty(items[length - 1], "scaleX") +
(parseFloat(config.paddingRight) || 0);
for (i = 0; i < length; i++) {
item = items[i];
curX = (xPercents[i] / 100) * widths[i];
distanceToStart = item.offsetLeft + curX - startX;
distanceToLoop =
distanceToStart + widths[i] * gsap.getProperty(item, "scaleX");
tl.to(
item,
{
xPercent: snap(((curX - distanceToLoop) / widths[i]) * 100),
duration: distanceToLoop / pixelsPerSecond
},
0
)
.fromTo(
item,
{
xPercent: snap(
((curX - distanceToLoop + totalWidth) / widths[i]) * 100
)
},
{
xPercent: xPercents[i],
duration:
(curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond,
immediateRender: false
},
distanceToLoop / pixelsPerSecond
)
.add("label" + i, distanceToStart / pixelsPerSecond);
times[i] = distanceToStart / pixelsPerSecond;
}
function toIndex(index, vars) {
vars = vars || {};
Math.abs(index - curIndex) > length / 2 &&
(index += index > curIndex ? -length : length); // always go in the shortest direction
let newIndex = gsap.utils.wrap(0, length, index),
time = times[newIndex];
if (time > tl.time() !== index > curIndex) {
// if we're wrapping the timeline's playhead, make the proper adjustments
vars.modifiers = { time: gsap.utils.wrap(0, tl.duration()) };
time += tl.duration() * (index > curIndex ? 1 : -1);
}
curIndex = newIndex;
vars.overwrite = true;
return tl.tweenTo(time, vars);
}
tl.next = (vars) => toIndex(curIndex + 1, vars);
tl.previous = (vars) => toIndex(curIndex - 1, vars);
tl.current = () => curIndex;
tl.toIndex = (index, vars) => toIndex(index, vars);
tl.times = times;
tl.progress(1, true).progress(0, true); // pre-render for performance
if (config.reversed) {
tl.vars.onReverseComplete();
tl.reverse();
}
return tl;
}
This Pen doesn't use any external CSS resources.