Carousel

Counter Belts

Two logo belts running in opposite directions with dissolved edges: counter-motion reads as "the list is long" rather than "a carousel is moving".

  • carousel
  • marquee
  • logos
  • infinite

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/carousel-012?lang=en

Trusted by

  • Северсталь
  • Ozon
  • Кофеин
  • Яндекс
  • Точка
  • Скиллбокс
  • Тинькофф
  • Авито
  • ВкусВилл
  • Самокат
  • Читай-город
  • Литрес
Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "carousel-012" (Counter Belts) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/carousel-012.json

Registry item: https://vibeui.ru/r/carousel-012.json
Installs to: components/vibeui/carousel-012.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
Two logo belts running in opposite directions with dissolved edges: counter-motion reads as "the list is long" rather than "a carousel is moving".

Two counter-running logo belts in pure CSS, pausing on hover and on focus. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Carousel012 } from "@/components/vibeui/carousel-012"

<Carousel012
  title="Trusted by"
  duration={34}
  topRow={["Northwind", "Ozon", "Caffeine"]}
  bottomRow={["Tinkoff", "Avito", "Samokat"]}
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-carousel-012-* palette — do not swap it for your theme tokens
- duplicating the list and shifting by exactly half: otherwise the seam is visible
- aria-hidden on the copy — the list must not be read twice by a screen reader
- pausing on hover and on :focus-within: from a keyboard the belt must freeze too
- the full stop under prefers-reduced-motion with a fallback to ordinary scrolling
- the edge mask: the belt has to emerge from nothing rather than break off

## 6. You may change
- the belt contents through topRow and bottomRow
- the heading through title
- the speed through duration, in seconds per full pass
- the chip colour in the --vibeui-carousel-012-chip variable

## 7. Rules
- The logos are text here: with images give them a fixed height or the belt will judder.
- Under twenty seconds per pass it flickers and disturbs the text next to it.
- The lists must be long enough to fill the width: a short one leaves a gap.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/carousel-012.json
https://vibeui.ru/r/carousel-012.json

Компонент самодостаточен: один файл, без зависимостей, палитра в локальных переменных --vibeui-carousel-012-*. Серверный: хуков нет, движение целиком на CSS-анимации. Каждая лента продублирована в разметке и сдвигается ровно на половину ширины с учётом зазора, поэтому шва между повторами не видно; копия помечена aria-hidden, чтобы список не прочитался дважды. Края растворены mask-image. Анимация замирает и при наведении, и при фокусе внутри блока, а при prefers-reduced-motion ленты останавливаются и превращаются в обычную горизонтальную прокрутку.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Carousel012Props = Omit<
  ComponentPropsWithoutRef<"section">,
  "children"
> & {
  /** Верхняя лента едет влево, нижняя — вправо. */
  topRow?: string[]
  bottomRow?: string[]
  title?: string
  /** Секунд на полный проход одной ленты. */
  duration?: number
}

// Идея компонента: две встречные ленты вместо одной. Встречное движение
// читается как «список большой», а не как «карусель едет»: глаз не успевает
// зацепиться за конкретный логотип и воспринимает полосу целиком. Каждая
// лента продублирована и сдвигается ровно на половину, поэтому шва нет;
// копия помечена aria-hidden, чтобы список не прочитался дважды. Движение
// замирает при наведении и при фокусе внутри блока, а при
// prefers-reduced-motion ленты стоят и просто прокручиваются пальцем.
const STYLES = `
:where([data-vibeui-block="carousel-012"]){
--vibeui-carousel-012-bg:oklch(1 0 0);
--vibeui-carousel-012-fg:oklch(0.34 0.014 265);
--vibeui-carousel-012-muted:oklch(0.58 0.014 265);
--vibeui-carousel-012-border:oklch(0.91 0.006 265);
--vibeui-carousel-012-chip:oklch(0.97 0.003 265);
--vibeui-carousel-012-duration:34s;
--vibeui-carousel-012-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="carousel-012"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:30rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-carousel-012-bg);
border:1px solid var(--vibeui-carousel-012-border);border-radius:1rem;
font-family:var(--vibeui-carousel-012-font);color:var(--vibeui-carousel-012-fg);
}
[data-vibeui-block="carousel-012"] [data-part="title"]{
margin:0 0 0.125rem;font-size:0.6875rem;font-weight:700;letter-spacing:0.08em;
text-transform:uppercase;color:var(--vibeui-carousel-012-muted);
}
/* Края растворены маской: лента должна выезжать из ничего, а не обрываться. */
[data-vibeui-block="carousel-012"] [data-part="window"]{
overflow:hidden;
mask:linear-gradient(to right,transparent,oklch(0 0 0) 10%,oklch(0 0 0) 90%,transparent);
}
[data-vibeui-block="carousel-012"] [data-part="rail"]{
display:flex;width:max-content;gap:0.5rem;margin:0;padding:0.25rem 0;list-style:none;
animation:vibeui-carousel-012-left var(--vibeui-carousel-012-duration) linear infinite;
}
[data-vibeui-block="carousel-012"] [data-part="rail"][data-way="right"]{
animation-name:vibeui-carousel-012-right;
}
/* Пауза и по наведению, и по фокусу: с клавиатуры лента тоже должна замереть. */
[data-vibeui-block="carousel-012"]:hover [data-part="rail"],
[data-vibeui-block="carousel-012"]:focus-within [data-part="rail"]{animation-play-state:paused}
[data-vibeui-block="carousel-012"] [data-part="chip"]{
flex:none;display:inline-flex;align-items:center;
padding:0.375rem 0.75rem;border-radius:0.5rem;
background:var(--vibeui-carousel-012-chip);
border:1px solid var(--vibeui-carousel-012-border);
font-size:0.8125rem;font-weight:650;letter-spacing:-0.01em;white-space:nowrap;
}
@keyframes vibeui-carousel-012-left{to{transform:translateX(calc(-50% - 0.25rem))}}
@keyframes vibeui-carousel-012-right{from{transform:translateX(calc(-50% - 0.25rem))}to{transform:translateX(0)}}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="carousel-012"] [data-part="rail"]{animation:none!important;transform:none!important}
[data-vibeui-block="carousel-012"] [data-part="window"]{overflow-x:auto;scrollbar-width:thin}
}
`

const DEFAULT_TOP = [
  "Северсталь",
  "Ozon",
  "Кофеин",
  "Яндекс",
  "Точка",
  "Скиллбокс",
]

const DEFAULT_BOTTOM = [
  "Тинькофф",
  "Авито",
  "ВкусВилл",
  "Самокат",
  "Читай-город",
  "Литрес",
]

function Rail({ items, way }: { items: string[]; way: "left" | "right" }) {
  return (
    <div data-part="window">
      <ul data-part="rail" data-way={way}>
        {items.map((item) => (
          <li data-part="chip" key={item}>
            {item}
          </li>
        ))}
        {items.map((item) => (
          <li data-part="chip" key={`echo-${item}`} aria-hidden="true">
            {item}
          </li>
        ))}
      </ul>
    </div>
  )
}

/**
 * Две встречные бегущие ленты логотипов с растворёнными краями и паузой.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Carousel012({
  topRow = DEFAULT_TOP,
  bottomRow = DEFAULT_BOTTOM,
  title = "Нам доверяют",
  duration = 34,
  className,
  style,
  ...props
}: Carousel012Props) {
  const palette = {
    "--vibeui-carousel-012-duration": `${duration}s`,
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-carousel-012" precedence="medium">
        {STYLES}
      </style>
      <section
        {...props}
        data-vibeui-block="carousel-012"
        aria-label={title}
        className={className}
        style={palette}
      >
        <p data-part="title">{title}</p>
        <Rail items={topRow} way="left" />
        <Rail items={bottomRow} way="right" />
      </section>
    </>
  )
}