Carousel
Um carousel componível para listas horizontais ou verticais de itens, construído sobre o Embla Carousel. Aceita itens de largura fixa ou fracionária, passos configuráveis, arrastar e swipe, loop, autoplay e controles acessíveis, estilizado só com tokens do design system.
Uso
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@medway-ui/core";
<Carousel aria-label="Grupos para descobrir">
<CarouselPrevious />
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
<CarouselNext />
</Carousel>Quando controls é "side", "floating" ou "outside", CarouselPrevious, CarouselContent e CarouselNext dividem uma única linha, com as setas centralizadas verticalmente na trilha, e todo outro filho (como CarouselDots) ocupa uma linha inteira só para ele, na ordem do documento. Na orientação vertical, os filhos se empilham na ordem do documento, então coloque CarouselPrevious antes de CarouselContent e CarouselNext depois.
Exemplos
Itens de largura fixa, um item por passo
Cards com largura fixa, gap de 12px e setas ao lado da trilha. As setas só aparecem a partir do breakpoint md, já que no mobile a lista é navegada por swipe. Nas bordas as setas ficam desabilitadas, e a trilha nunca muda de largura. Como as setas ficam ocultas em telas pequenas, o CarouselDots é renderizado abaixo de md (ou use outro controle que funcione com um único toque) para que a lista possa ser navegada sem arrastar (WCAG 2.5.7).
Passo por página
Com step="page", cada clique avança quantos itens couberem na trilha. O último passo sempre encosta o último item no fim da trilha, sem espaço vazio. Quando não há mais itens numa direção, a seta dessa direção fica desabilitada e esmaecida; use edgeBehavior="hide" para ocultá-la. A trilha também ganha um fade nas bordas que têm mais conteúdo.
Passo de N itens
Passe um número para step para avançar exatamente essa quantidade de itens por clique. Não use um valor maior que a quantidade de itens que cabem na trilha: um passo maior deixa alguns itens que nunca chegam ao início da trilha. Use "page" para avançar quantos itens couberem. Aqui cada clique avança 3 itens, e cada dot representa um passo. Como em qualquer tamanho de passo, o último passo encosta o último item no fim da trilha.
Banner com autoplay, loop e dots
O autoplay pausa enquanto o ponteiro está sobre o carousel e enquanto a aba do navegador está em segundo plano, e retoma depois. Quando um elemento dentro do carousel recebe foco pelo teclado, a rotação para e continua parada até o usuário retomá-la com o CarouselAutoplayToggle, como recomenda o padrão de carousel do WAI-ARIA. Depois que o usuário retoma, o foco pelo teclado não a interrompe mais. O toggle permite pausar e retomar a rotação a qualquer momento (WCAG 2.2.2); coloque-o antes do conteúdo para que seja o primeiro elemento na ordem de tabulação. Navegar pelas setas, pelo teclado ou pelos dots reinicia o timer do autoplay. Clicar no dot atual realinha o slide (útil depois de um arraste livre) sem reiniciar o timer. Com prefers-reduced-motion, o autoplay começa pausado e a navegação é instantânea, inclusive a rotação que o usuário inicia pelo toggle. Por padrão, o toggle fica acima dos slides, então dá para posicioná-lo sobre eles com containerClassName (por exemplo, "absolute right-3 top-3").
Transição fade
Use transition="fade" para trocar os slides com fade cruzado em vez de deslizá-los. Arrastar e swipe continuam funcionando.
Itens por breakpoint
O tamanho do item vem do CSS, então o layout já sai correto na renderização no servidor. Aqui aparece um item no mobile, dois no tablet e três no desktop, com setas flutuando sobre a trilha.
Vertical
Com orientation="vertical", as setas vão para cima e para baixo da trilha e o teclado usa ↑ e ↓. Defina uma altura para o CarouselContent.
Controlado com setApi
O setApi dá acesso à API do Embla: scrollPrev, scrollNext, scrollTo, canScrollPrev, canScrollNext, selectedScrollSnap, scrollSnapList e eventos como select e reInit. Para uma posição controlada, use index com onIndexChange. Para controles personalizados renderizados dentro do Carousel, prefira useCarousel(): o scrollTo dele respeita prefers-reduced-motion e reinicia o timer do autoplay quando a posição muda, o que chamadas diretas ao Embla não fazem.
Personalização
Os itens não têm estilo visual, só layout (tamanho e gap), então você pode colocar qualquer conteúdo dentro do CarouselItem. Cada parte aceita classes para os próprios elementos:
| Parte | Propriedade | Elemento |
|---|---|---|
Carousel | className | Raiz |
CarouselContent | viewportClassName / className / fadeClassName | Viewport / trilha / os dois fades das bordas |
CarouselItem | className | Item |
CarouselPrevious / CarouselNext | containerClassName / className, além de size e variant | Wrapper / botão |
CarouselDots | className / dotClassName / indicatorClassName | Grupo / área clicável de cada dot / marca visível |
CarouselAutoplayToggle | containerClassName / className, além de size e variant | Wrapper / botão |
Em indicatorClassName, classes simples estilizam os dots inativos e classes data-[active=true]: estilizam o ativo. A linha de dots reserva 24px de altura antes de o carousel ficar pronto; se você aumentar os dots, reserve a altura deles com className (ex.: min-h-8) para o layout não deslocar. Em controls="floating", as setas outlined padrão ganham um fundo de superfície opaco; as outras variantes mantêm o próprio fundo.
Acessibilidade
- A raiz tem
role="region"comaria-roledescription="carousel", e oaria-labelé obrigatório. - Cada item tem
role="group",aria-roledescription="slide"e um label no formato "N de total" (dá para sobrescrever comaria-label). - ← e → (↑ e ↓ na vertical) movem o carousel enquanto o foco está dentro dele, exceto quando o foco está num elemento que usa essas teclas: campos de texto, selects, players de vídeo e áudio, widgets compostos (sliders, listboxes, tabs, menus, grids) e áreas que rolam no mesmo eixo do carousel, inclusive dentro de shadow DOM.
- Quando um clique ou uma tecla de seta na seta focada a leva até uma borda, o foco vai para a seta oposta sem rolar a página. Autoplay, arraste e mudanças de
indexnunca movem o foco, exceto comtransition="fade"(veja abaixo). - Teclas de seta pressionadas com Alt, Ctrl, Meta ou Shift, e teclas de seta pressionadas dentro de conteúdo renderizado em portal (popovers, dialogs), ficam por conta do navegador e desse conteúdo.
- Uma live region visualmente oculta anuncia a posição após cada navegação: "Slide N de total", ou "Página N de total" quando cada posição agrupa vários itens. Nada é anunciado no carregamento, quando itens são adicionados ou removidos, nem quando uma mudança de opção faz o carousel restaurar a posição, e ela fica em silêncio enquanto o autoplay está girando.
- O grupo de dots tem o label "Escolher slide", ou "Escolher página" quando cada posição agrupa vários itens, e o dot ativo tem
aria-current="true". - Com
transition="fade", os slides fora da posição atual ficaminerte ocultos para leitores de tela. Se o foco estava dentro de um slide que some, ele vai para o slide que aparece (o primeiro que não estáinert), seja qual for a causa da mudança, inclusive o autoplay; quando o usuário está navegando pelo teclado, o slide focado mostra o anel de foco. Uminertque você define num item é respeitado, inclusive quando muda depois. - Itens envolvidos num
Fragmentsão numerados como se fossem filhos diretos doCarouselContent. - Tab percorre os itens e a trilha rola até o item focado.
- Arrastar nunca dispara o clique de botões e links dentro dos itens.
- Setas ocultas por
edgeBehavior="hide"ouhideControlsWhenFitsusamvisibility: hidden, então saem da ordem de tabulação sem mudar o layout. Abaixo do breakpoint decontrolsFrom, elas saem do layout (display: none), e a trilha ocupa a largura das setas.
API
Estrutura do Carousel
| Componente | Descrição | Uso |
|---|---|---|
| Carousel | Componente raiz, dono da instância do Embla e do estado do carousel | Contém todos os outros componentes do carousel |
| CarouselContent | Viewport e trilha que contêm os itens | Envolve a lista de CarouselItem |
| CarouselItem | Um slide | Filho direto de CarouselContent; defina o tamanho com classes basis-* |
| CarouselPrevious / CarouselNext | Botões de seta construídos sobre o Button | Opcional; posicionado pela prop controls do Carousel |
| CarouselDots | Um dot por posição de rolagem | Opcional |
| CarouselAutoplayToggle | Pausa e retoma o autoplay | Opcional; só renderiza quando o autoplay está ativo |
Carousel
| Propriedade | Tipo | Valores possíveis | Padrão | Descrição |
|---|---|---|---|---|
| aria-label | string | Qualquer string | - | Obrigatório. Nome acessível da região do carousel. Não inclua a palavra "carousel". |
| orientation | CarouselOrientation | "horizontal", "vertical" | "horizontal" | Eixo de rolagem. |
| step | number | "page" | 1, 2, 3..., "page" | 1 | Quantos itens cada passo avança. "page" avança quantos itens couberem na trilha. Valores abaixo de 1 ou não finitos viram 1, e frações são truncadas. |
| align | CarouselAlign | "start", "center", "end" | "start" | Alinhamento dos itens dentro da trilha. |
| loop | boolean | true, false | false | Volta para o primeiro item depois do último. |
| dragFree | boolean | true, false | false | Rolagem livre com inércia em vez de encaixar num item. |
| transition | CarouselTransition | "slide", "fade" | "slide" | Como os itens trocam. "fade" é pensado para um item por vez. |
| autoplay | boolean | { delay?: number } | true, false, { delay: 5000 } | false | Gira automaticamente. delay é em milissegundos (padrão 5000); um delay que não seja número positivo vira 5000. |
| controls | CarouselControls | "side", "floating", "outside", "inline" | "side" | Onde as setas ficam: ao lado da trilha, flutuando sobre ela, fora dos limites do carousel ou onde você as colocar ("inline"). "outside" precisa de espaço livre dos dois lados do carousel; em layouts de largura total, combine com controlsFrom. |
| controlsFrom | CarouselControlsFrom | "always", "md", "lg" | "always" | Breakpoint a partir do qual as setas aparecem. Abaixo dele, renderize o CarouselDots para a lista não depender de arraste. |
| edgeBehavior | CarouselEdgeBehavior | "disable", "hide" | "disable" | O que a seta faz quando não há mais nada na direção dela: "disable" a mantém visível, desabilitada e esmaecida; "hide" a oculta. Nenhuma das opções muda o layout. |
| hideControlsWhenFits | boolean | true, false | true | Oculta setas, dots e o toggle de autoplay quando todos os itens cabem na trilha. |
| index | number | 0, 1, 2... | - | Posição de rolagem controlada (índice do scroll snap). Se o pai renderizar de novo sem atualizar index depois do onIndexChange, o carousel volta para index. Um pai que atualiza index mais tarde (por exemplo, depois de uma requisição) não é desfeito nesse meio-tempo, desde que não renderize de novo com o index antigo antes disso. O carousel reconhece a renderização do pai pelas props, então um pai que renderiza de novo com exatamente as mesmas props (children e callbacks memoizados) não é percebido e o carousel mantém a posição. |
| defaultIndex | number | 0, 1, 2... | 0 | Posição de rolagem inicial no modo não controlado. |
| onIndexChange | (index: number) => void | - | - | Função chamada quando a posição de rolagem selecionada muda por interação do usuário, autoplay ou remoção de itens. Não é chamada quando a mudança vem de index. |
| setApi | (api: CarouselApi) => void | - | - | Recebe a API do Embla quando o carousel fica pronto. |
| opts | CarouselOptions | Opções do Embla | - | Opções extras do Embla (ex.: duration, breakpoints). As props dedicadas acima têm prioridade sobre as opções base, mas as opções dentro de breakpoints as sobrescrevem enquanto a media query correspondente estiver ativa. |
| className | string | Qualquer classe CSS | - | Classes CSS adicionais para a raiz. |
CarouselContent
| Propriedade | Tipo | Valores possíveis | Padrão | Descrição |
|---|---|---|---|---|
| gap | CarouselGap | "none", "xs", "sm", "md", "lg", "xl" | "md" | Espaço entre os itens: 0, 8, 12, 16, 24 ou 32px. |
| fadeEdges | boolean | true, false | false | Aplica fade nas bordas que ainda têm conteúdo para rolar, como nas Tabs. |
| fadeClassName | string | Qualquer classe CSS | - | Classes CSS adicionais para os dois fades das bordas, ex.: from-surface-card quando o carousel fica sobre um card. |
| viewportClassName | string | Qualquer classe CSS | - | Classes CSS adicionais para o viewport (o elemento que recorta a trilha). |
| className | string | Qualquer classe CSS | - | Classes CSS adicionais para a trilha. |
CarouselItem
Aceita todas as props de div. Defina o tamanho com classes como basis-full, basis-1/3, md:basis-1/2 ou uma largura fixa.
CarouselPrevious / CarouselNext
Aceitam todas as props de Button, exceto iconLeft, iconRight e children. Use size ("sm", "md", "lg" ou "xl", os mesmos tamanhos do Button) para mudar o tamanho da seta; as setas mantêm a posição em todo modo de controls e em toda orientação. Use className para estilizar o botão e containerClassName para estilizar o elemento que o envolve. A variant padrão é "outlined", o size padrão é "md" e o aria-label padrão é "Anterior" / "Próximo". Um onClick que chama event.preventDefault() cancela a navegação. disabled se soma ao estado de borda: pode desabilitar uma seta, mas nunca habilita uma seta numa borda. Nos modos "floating" e "outside", uma seta desabilitada continua bloqueando cliques, então eles nunca chegam ao slide embaixo dela.
<CarouselPrevious size="lg" variant="regular-soft" className="shadow-level-1" />CarouselDots
| Propriedade | Tipo | Valores possíveis | Padrão | Descrição |
|---|---|---|---|---|
| getDotLabel | (index: number, total: number) => string | - | "Ir para o slide N" / "Ir para a página N" | Label acessível de cada dot. Por padrão, os dots falam em páginas quando cada posição agrupa vários itens (um step maior que 1, ou "page" com mais de um item por página). Caso contrário, cada dot recebe o nome do primeiro slide que a posição dele mostra. |
| dotClassName | string | Qualquer classe CSS | - | Classes CSS adicionais para a área clicável de cada dot (24px por padrão). |
| indicatorClassName | string | Qualquer classe CSS | - | Classes CSS adicionais para a marca visível dentro de cada dot. Classes simples estilizam os dots inativos; use o prefixo data-[active=true]: para estilizar o ativo. |
| className | string | Qualquer classe CSS | - | Classes CSS adicionais para o grupo de dots. |
CarouselAutoplayToggle
Aceita todas as props de Button, exceto iconLeft, iconRight e children. A variant padrão é "outlined" e o size padrão é "sm". O label alterna entre "Pausar rotação automática" e "Iniciar rotação automática".
useCarousel
Hook que retorna o estado do carousel (selectedIndex, snapCount, canScrollPrev, canScrollNext, isScrollable, autoplay) e as ações (scrollPrev, scrollNext, scrollTo) para montar controles personalizados dentro do Carousel. O tipo de retorno é CarouselState.