SHARE
Eine Schritt-für-Schritt-Anleitung zum Erstellen interaktiver, responsiver horizontaler Marquees
In der heutigen digitalen Welt können ansprechende Website-Animationen die Benutzererfahrung erheblich verbessern. Ein besonders auffälliger Effekt ist die unendlich laufende horizontale Marquee-Animation, die mit Elementor Pro, CSS und GSAP erstellt werden kann. In diesem Tutorial zeige ich dir, wie du eine flüssige, interaktive und vollständig responsive Marquee-Animation baust – ideal für Logos, Texte oder andere scrollende Inhalte.
Diese Animation bietet eine leistungsstarke und flexible Funktionalität auf allen Geräten:
Die Basis der Marquee-Animation besteht aus mehreren wichtigen Elementen:
marquee-container
zumarquee-content
hinzufügenDie Marquee nutzt das Icon-Box-Widget, kann aber auch mit Bild- und Überschriften-Widgets verwendet werden. Wichtige Schritte:
<div>
setzenmarquee-item
hinzufügenDie Animation kann individuell angepasst werden:
GSAP (GreenSock Animation Platform) sorgt für die flüssige Animation:
Beim Implementieren der Marquee-Animation solltest du Folgendes beachten:
Diese Marquee-Animation ist eine professionelle, interaktive Ergänzung, die deine Website ansprechender macht und gleichzeitig voll responsiv bleibt. Die Kombination aus Elementor Pro’s Flexibilität und GSAP’s Animationstechnologie ermöglicht eine flüssige, individuell anpassbare Scroll-Animation, die für verschiedene Anwendungen geeignet ist.
Das Beste daran? Diese Lösung ist extrem vielseitig – egal, ob du Kundenlogos präsentierst, wichtige Nachrichten hervorhebst oder Produktinformationen zeigst, die Marquee-Animation kann perfekt an deine Bedürfnisse angepasst werden und sieht auf allen Geräten professionell aus.
Indem du diese Schritte befolgst und die Parameter an deine Website anpasst, kannst du eine interaktive, visuell ansprechende Marquee erstellen, die das Nutzererlebnis verbessert. 🚀
clamp()
-Funktion für Größen, wenn du die Marquees erstellst. Nutze stattdessen Pixel oder REM – keine %
oder vw
, da das die Animation zerstören kann.
Grundsätzlich ist dieser Code für ein Marquee auf der Seite optimiert. Ich habe aber dennoch zusätzlichen Code für eine zweite Marquee Animation unten eingefügt.
Falls es eine generelle Anforderung wäre, zwei Marquees auf einer Seite zu haben, müsste der Code dafür optimiert werden. Das war aber nicht der Fokus dieser Demo – ich fand nur, dass es cool aussieht. 😃
Ändere einfach die Klassen in den Widgets wie folgt:
marquee-container2
marquee-content2
marquee-item2
Dann füge den CSS- und GSAP-Code ein, den du weiter unten unter dem Titel „Second Marquee“ findest.
Füge den Code in das HTML Widget
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/gsap.min.js"></script>
<script>
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;
}
let mm = gsap.matchMedia();
mm.add("(min-width: 1025px) and (prefers-reduced-motion: no-preference)", () => restartMarquee(1, 0)); //change here the speed and padding right for desktop//
mm.add("(min-width: 768px) and (max-width: 1024px) and (prefers-reduced-motion: no-preference)", () => restartMarquee(1, 0));//change here the speed and padding right for tablet//
mm.add("(max-width: 767px) and (prefers-reduced-motion: no-preference)", () => restartMarquee(0.8, 0)); //change here the speed and padding right for mobile//
function restartMarquee(speed, paddingRight) {
// 🎯 Prüfe, ob der Nutzer reduzierte Bewegung bevorzugt
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) {
console.log("🛑 prefers-reduced-motion erkannt! Marquee wird NICHT gestartet.");
if (window.loop) {
window.loop.kill();
window.loop = null;
}
// Alle Animationen der Marquee-Elemente stoppen und statisch setzen
gsap.killTweensOf(".marquee-item");
gsap.set(".marquee-item", { clearProps: "all" });
document.querySelectorAll(".marquee-item").forEach(item => {
item.style.transform = "translate(0, 0)";
item.style.animation = "none";
});
return; // Beende die Funktion hier, damit keine neue Animation gestartet wird
}
if (window.loop) {
window.loop.kill(); // Stoppe die alte Animation
window.loop = null;
}
let direction = 1;
const marqueeContainer = document.querySelector(".marquee-container");
if (marqueeContainer) {
marqueeContainer.addEventListener("mouseenter", () => {
if (window.loop) window.loop.pause();
});
marqueeContainer.addEventListener("mouseleave", () => {
if (window.loop) {
direction *= -1;
gsap.to(window.loop, { timeScale: direction, overwrite: true });
window.loop.play();
}
});
marqueeContainer.addEventListener("touchstart", (e) => {
e.preventDefault();
if (window.loop) window.loop.pause();
});
marqueeContainer.addEventListener("touchend", () => {
if (window.loop) {
direction *= -1;
gsap.to(window.loop, { timeScale: direction, overwrite: true });
window.loop.play();
}
});
}
gsap.set(".marquee-item", { clearProps: "all" });
setTimeout(() => {
window.loop = horizontalLoop(".marquee-item", {
speed: speed,
repeat: -1,
paused: false,
paddingRight: paddingRight,
});
}, 100);
}
let resizeTimeout;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
mm.revert(); // Reset MatchMedia animations
restartMarqueeForCurrentMedia(); // Restart marquee with updated sizes
}, 200);
});
function restartMarqueeForCurrentMedia() {
let speed = 1;
let paddingRight = 0;
if (window.matchMedia("(min-width: 1024px) and (prefers-reduced-motion: no-preference)").matches) {
speed = 1;
paddingRight = 0;
} else if (window.matchMedia("(min-width: 768px) and (max-width: 1023px) and (prefers-reduced-motion: no-preference)").matches) {
speed = 1;
paddingRight = 0;
} else if (window.matchMedia("(max-width: 767px) and (prefers-reduced-motion: no-preference)").matches) {
speed = 0.8;
paddingRight = 0;
}
restartMarquee(speed, paddingRight);
}
</script>
Füge diesen Code in dein Custom CSS von WordPress oder Elementor oder in eines deiner Widgets unter Advanced Custom CSS
* {
box-sizing: border-box;
}
.marquee-container {
width: 100% !important;
overflow: hidden !important;
white-space: nowrap !important;
position: relative !important;
}
.marquee-content {
display: flex !important;
}
.marquee-item {
flex: 0 0 auto !important;
padding: 0 0 !important;
max-width: none !important;
margin: 0 !important;
justify-content: center !important;
}
/* Desktop and General Settings, will be overwritten for Tablet and Mobile with Media Queries below */
/* Spacing between icon and title (horizontal) */
.marquee-item > .elementor-widget-container > .elementor-icon-box-wrapper > .elementor-icon-box-icon {
margin-right: 40px !important;
line-height: 0 !important;
}
/* Tablet Settings */
@media (max-width: 1024px) {
/* 4️⃣ 🛠 Fix for mobile: Removes unnecessary space caused by the Content-Box and aligns icon and heading */
.marquee-item .elementor-widget-container .elementor-icon-box-wrapper {
text-align: center !important;
}
/* Spacing between icon and title (horizontal) */
.marquee-item > .elementor-widget-container > .elementor-icon-box-wrapper > .elementor-icon-box-icon {
margin-right: 30px !important;
line-height: 0 !important;
}
}
/* Mobile Settings */
@media (max-width: 767px) {
/* 4️⃣ 🛠 Fix for mobile: Removes unnecessary space caused by the Content-Box and aligns icon and heading */
.marquee-item .elementor-widget-container .elementor-icon-box-wrapper .elementor-icon-box-content {
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* Spacing between icon and title (horizontal) */
.marquee-item > .elementor-widget-container > .elementor-icon-box-wrapper > .elementor-icon-box-icon {
margin-right: 30px !important;
line-height: 0 !important;
}
}
Hier findet ihr den Code für das zusätzliche Marquee. Dieses ist bereits mit reversed: true voreingestellt, so dass das Marquee zunächst in umgekehrter Richtung wie das erste Marquee startet.
* {
box-sizing: border-box;
}
.marquee-container2 {
width: 100% !important;
overflow: hidden !important;
white-space: nowrap !important;
position: relative !important;
}
.marquee-content2 {
display: flex !important;
}
.marquee-item2 {
flex: 0 0 auto !important;
padding: 0 0 !important;
max-width: none !important;
margin: 0 !important;
justify-content: center !important;
}
/* Desktop and General Settings, will be overwritten for Tablet and Mobile with Media Queries below */
/* Spacing between icon and title (horizontal) */
.marquee-item2 > .elementor-widget-container > .elementor-icon-box-wrapper > .elementor-icon-box-icon {
margin-right: 40px !important;
line-height: 0 !important;
}
/* Tablet Settings */
@media (max-width: 1024px) {
/* 4️⃣ 🛠 Fix for mobile: Removes unnecessary space caused by the Content-Box and aligns icon and heading */
.marquee-item2 .elementor-widget-container .elementor-icon-box-wrapper {
text-align: center !important;
}
/* Spacing between icon and title (horizontal) */
.marquee-item2 > .elementor-widget-container > .elementor-icon-box-wrapper > .elementor-icon-box-icon {
margin-right: 30px !important;
line-height: 0 !important;
}
}
/* Mobile Settings */
@media (max-width: 767px) {
/* 4️⃣ 🛠 Fix for mobile: Removes unnecessary space caused by the Content-Box and aligns icon and heading */
.marquee-item2 .elementor-widget-container .elementor-icon-box-wrapper .elementor-icon-box-content {
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* Spacing between icon and title (horizontal) */
.marquee-item2 > .elementor-widget-container > .elementor-icon-box-wrapper > .elementor-icon-box-icon {
margin-right: 30px !important;
line-height: 0 !important;
}
}
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/gsap.min.js"></script>
<script>
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;
}
let mm2 = gsap.matchMedia();
mm2.add("(min-width: 1024px) and (prefers-reduced-motion: no-preference)", () => restartMarquee2(1, 0));
mm2.add("(min-width: 768px) and (max-width: 1023px) and (prefers-reduced-motion: no-preference)", () => restartMarquee2(1, 0));
mm2.add("(max-width: 767px) and (prefers-reduced-motion: no-preference)", () => restartMarquee2(0.8, 0));
function restartMarquee2(speed2, paddingRight2) {
// 🎯 Prüfe, ob der Nutzer reduzierte Bewegung bevorzugt
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) {
console.log("🛑 prefers-reduced-motion erkannt! Marquee wird NICHT gestartet.");
if (window.loop2) {
window.loop2.kill();
window.loop2 = null;
}
// Alle Animationen der Marquee-Elemente stoppen und statisch setzen
gsap.killTweensOf(".marquee-item2");
gsap.set(".marquee-item2", { clearProps: "all" });
document.querySelectorAll(".marquee-item2").forEach(item => {
item.style.transform = "translate(0, 0)";
item.style.animation = "none";
});
return; // Beende die Funktion hier, damit keine neue Animation gestartet wird
}
if (window.loop2) {
window.loop2.kill(); // Stoppe die alte Animation
window.loop2 = null;
}
let direction = 1;
const marqueeContainer2 = document.querySelector(".marquee-container2");
if (marqueeContainer2) {
marqueeContainer2.addEventListener("mouseenter", () => {
if (window.loop2) window.loop2.pause();
});
marqueeContainer2.addEventListener("mouseleave", () => {
if (window.loop2) {
direction *= -1;
gsap.to(window.loop2, { timeScale: direction, overwrite: true });
window.loop2.play();
}
});
marqueeContainer2.addEventListener("touchstart", (e) => {
e.preventDefault();
if (window.loop2) window.loop2.pause();
});
marqueeContainer2.addEventListener("touchend", () => {
if (window.loop2) {
direction *= -1;
gsap.to(window.loop2, { timeScale: direction, overwrite: true });
window.loop2.play();
}
});
}
gsap.set(".marquee-item2", { clearProps: "all" });
setTimeout(() => {
window.loop2 = horizontalLoop(".marquee-item2", {
speed: speed2,
repeat: -1,
paused: false,
reversed: true,
paddingRight: paddingRight2,
});
}, 100);
}
let resizeTimeout2;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout2);
resizeTimeout2 = setTimeout(() => {
mm2.revert(); // Reset MatchMedia animations
restartMarqueeForCurrentMedia2(); // Restart marquee with updated sizes
}, 200);
});
function restartMarqueeForCurrentMedia2() {
let speed2 = 1;
let paddingRight2 = 0;
if (window.matchMedia("(min-width: 1024px) and (prefers-reduced-motion: no-preference)").matches) {
speed = 1;
paddingRight = 0;
} else if (window.matchMedia("(min-width: 768px) and (max-width: 1023px) and (prefers-reduced-motion: no-preference)").matches) {
speed = 1;
paddingRight = 0;
} else if (window.matchMedia("(max-width: 767px) and (prefers-reduced-motion: no-preference)").matches) {
speed = 0.8;
paddingRight = 0;
}
restartMarquee2(speed2, paddingRight2);
}
</script>
Folge Uns
Angebot
© Copyright Lechclick 2025
Den digitalen Masterplan bekommst du als Dankeschön für dein Newsletter Abo.
*Pflichtfeld. Du kannst dich jederzeit mit einem Klick wieder abmelden. Meine E-Mails enthalten neben zahlreichen kostenlosen Tipps und Inhalten auch Informationen zu meinen Produkten, Angeboten, Aktionen und zu meinem Unternehmen. Hinweise zum Datenschutz erhältst du unter Datenschutz.