Membangun Marquee 3 Kolom yang Accessible dengan CSS dan React
Marquee 3 kolom x 3 pilar pakai CSS keyframes + React: seamless loop, pause-on-hover, RTL-ready, aria-hidden/inert untuk accessibility -- tanpa library carousel.
Ringkasan
Pakai CSS keyframes buat marquee. Ngga pakai library carousel karena berat. Hasilnya tiga pilar marquee vertikal.
Dari Library Carousel ke Custom CSS
Saya mulai baca commit 775e3df -- "seksi Artikel Populer marquee 3 kolom di 3 pilar (t_populer9)". Tebakan pertama saya: pakai library carousel jadi, npm install selesai. Realita: library carousel (Embla, Swiper) terlalu berat dan nggak punya marquee mode bawaan yang infinite loop + pause-on-hover + 3 kolom sekaligus. Saya mesti bangun sendiri pakai CSS keyframes + React.
Tiga Masalah Utama
Masalah utamanya tiga: (1) seamless loop -- marquee harus putar terus tanpa jeda terlihat; (2) accessibility -- duplikat konten buat loop nggak boleh dibaca screen reader, keyboard nggak boleh tab ke duplikat; (3) RTL support -- animasi harus balik arah kalau dir="rtl".
Sumber [5] (Hirael) nyatakan: "Infinite scrolling row or column for logos and testimonials, with pause-on-hover, reverse and vertical modes. Keyframes ship inline, zero config." Sumber [6] (beUI) tambah: "The content is duplicated to make the loop seamless, and the copy is hidden from screen readers and made inert, so assistive technology reads each item once and keyboard users do not tab through duplicates."
Pola Duplicate Tracks & Tiga Pilar
Pola yang saya ambil: dua track identik ([0, 1].map()), track kedua aria-hidden={true} inert={true}. CSS keyframes @keyframes marquee-x pakai calc(var(--marquee-x-dir, -1) * (100% + var(--marquee-gap))) biar RTL otomatis balik lewat --marquee-x-dir: 1 [5]. Pause-on-hover lewat [data-pause="true"]:hover [data-slot="marquee-track"] { animation-play-state: paused; } [5].
Tiga pilar artinya tiga instance marquee vertikal, masing-masing 3 kolom horizontal. Data Artikel Populer diambil per pilar (view 7 hari + random filler [commit 40ade74]), lalu di-shuffle dan dipotong 9 item per pilar. Setiap pilar jadi satu marquee vertikal dengan 3 kolom -- kolom kiri, tengah, kanan masing-masing track horizontal sendiri.
Alasan Nggak Pakai Library
Performa: CSS-only animation (transform) jalan di compositor thread, 60 fps tanpa main-thread blocking. Library carousel bawa JS loop, resize observer, drag handler -- overhead buat marquee murni. Maintainability: inline keyframes di komponen [5], nggak butuh globals.css edit. Accessibility: aria-hidden + inert di duplikat [6] -- library jarang handle ini bawaan.
Satu detail yang gampang terlewat: gap harus di wrapper dan track biar seam antara dua track cocok [6]. Lupa ini -- jeda visual tiap loop.
// components/marquee.tsx
import { Children, type ReactNode } from "react";
import { cn } from "@/lib/utils";
export interface MarqueeProps {
children: ReactNode;
direction?: "left" | "right" | "up" | "down";
speed?: number;
pauseOnHover?: boolean;
gap?: string;
className?: string;
fade?: boolean;
}
export function Marquee({
children,
direction = "left",
speed = 30,
pauseOnHover = true,
gap = "1rem",
className,
fade = true,
}: MarqueeProps) {
const vertical = direction === "up" || direction === "down";
const reverse = direction === "right" || direction === "down";
const items = Children.toArray(children);
return (
{[0, 1].map((dup) => (
{items.map((child, i) => (
{child}
))}
))}
);
}
/* globals.css -- keyframes diletakkan di sini supaya bisa dipakai multi-marquee */
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(calc(-100% - var(--gap))); }
}
@keyframes marquee-vertical {
from { transform: translateY(0); }
to { transform: translateY(calc(-100% - var(--gap))); }
}
.animate-marquee { animation: marquee var(--duration, 30s) linear infinite; }
.animate-marquee-vertical { animation: marquee-vertical var(--duration, 30s) linear infinite; }
Hasilnya
Hasilnya: tiga pilar, masing-masing marquee vertikal 3 kolom, pause-on-hover, RTL-ready, accessible, nggak ada library ekstra. Commit 775e3df + t_populer9 + 40ade74 (sort popular) + a5f93eb (acak sampai 9) + 65ed6ab (auto-thumb YouTube) -- semuanya nyatu jadi satu fitur "Artikel Populer" yang hidup, nggak statis.
Sources
- [1] Meilisearch, Indexing best practices, diakses 26 Oktober 2026.
- [2] Meilisearch, Zero downtime index deployment, diakses 26 Oktober 2026.
- [3] Elastic, Reindex indices examples, diakses 26 Oktober 2026.
- [4] Wavise OpenLLM, Laravel Scout + Meilisearch in Docker, diakses 26 Oktober 2026.
- [5] Hirael, Marquee component, diakses 26 Oktober 2026.
- [6] beUI, React Marquee Component: Infinite Scroll Loop, diakses 26 Oktober 2026.