OGANIKO DESIGN SYSTEM

Carousel os-004

Also known as: Slider, Horizontal scroll, Card scroller

Horizontally scrollable card container with navigation arrows and an optional action link.

Example
Use the arrows or swipe to scroll. Arrows disable at edges.
How it works
A horizontal flex container with overflow-x: auto and hidden scrollbars. Items have a fixed width and flex-shrink: 0 to prevent collapse. On desktop, the container is sized so that 3 full items are visible plus a peek (~15%) of the next item, signaling more content. Navigation arrows in the top-right of the header row scroll the container by 80% of its visible width using scrollTo({ behavior: 'smooth' }). Arrows disable at the scroll edges (opacity drops to 0.3). An optional “View All” action link sits to the right of the arrows. On mobile (<768px), arrows are hidden and native touch scrolling takes over. On tablet (768–1023px), 2 items plus a peek are visible.
Variants
default

3 items visible + peek on desktop, 2 + peek on tablet, 1 + peek on mobile. Navigation arrows in header row.

compact

4 items visible + peek. For smaller cards (images, thumbnails). Same scroll mechanics.

Relationships
↔ os-001 Header
Parameters
NameTypeDefaultDescription
titlestring—Section heading text
subtitlestring—Section subheading text
itemsarray[]Carousel items (content defined by consumer)
actionLabelstring—Text for the “View All” link (omit to hide)
actionHrefstring#Destination for the action link
scrollAmountnumber0.8Fraction of container width to scroll per click (0–1)
classNamestring""Additional CSS classes on the outer container
Tokens
--color-text --color-text-secondary --color-border --color-bg --color-bg-muted --spacing-4 --spacing-6 --spacing-8 --spacing-12 --container-max --font-primary --weight-bold --weight-semibold --tracking-tight --tracking-caps
Code
os-004 · Vanilla JS scroll controller ▾
// Carousel controller — vanilla JS
// Attach to any element with [data-carousel]
function initCarousel(el) {
  const track = el.querySelector('[data-carousel-track]');
  const prev  = el.querySelector('[data-carousel-prev]');
  const next  = el.querySelector('[data-carousel-next]');
  if (!track || !prev || !next) return;

  const scrollAmount = 0.8; // 80% of visible width

  function checkEdges() {
    const { scrollLeft, scrollWidth, clientWidth } = track;
    prev.disabled = scrollLeft < 10;
    next.disabled = scrollLeft + clientWidth >= scrollWidth - 10;
  }

  prev.addEventListener('click', () => {
    track.scrollTo({
      left: track.scrollLeft - track.clientWidth * scrollAmount,
      behavior: 'smooth'
    });
  });

  next.addEventListener('click', () => {
    track.scrollTo({
      left: track.scrollLeft + track.clientWidth * scrollAmount,
      behavior: 'smooth'
    });
  });

  track.addEventListener('scroll', checkEdges);
  window.addEventListener('resize', checkEdges);
  checkEdges();
}
os-004 · React: SparksCarousel (OG-017 reference) ▾
<SparksCarousel
  title="Featured Items"
  subtitle="A collection of things"
  actionLabel="View All"
  actionHref="/items"
  items={[
    { id: 1, imageSrc: '/img/1.jpg', title: 'Item One', subtitle: 'Cat A' },
    { id: 2, imageSrc: '/img/2.jpg', title: 'Item Two', subtitle: 'Cat B' },
  ]}
/>