Carousel
Display and navigate through multiple content items.
1
2
3
4
5
6
7
8
9
import { component$ } from '@builder.io/qwik';
import { Card, Carousel } from '@qwik-ui/styled';
export default component$(() => {
return (
<Carousel.Root class="max-w-xs">
<Carousel.Scroller class="m-2">
{Array.from({ length: 9 }).map((_, index) => (
<Carousel.Slide key={index}>
<div class="px-2 py-3">
<Card.Root>
<Card.Content class="flex aspect-square items-center justify-center">
<span class="text-4xl font-semibold">{index + 1}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Slide>
))}
</Carousel.Scroller>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root>
);
});
Installation
Run the following cli command or copy/paste the component code into your project
qwik-ui add carousel
import {
useContext,
component$,
createContextId,
PropsOf,
Slot,
useContextProvider,
Signal,
} from '@builder.io/qwik';
import { Carousel as HCarousel } from '@qwik-ui/headless';
import { cn } from '@qwik-ui/utils';
import { buttonVariants } from '../button/button';
import { VariantProps } from 'class-variance-authority';
import {
LuChevronDown,
LuChevronLeft,
LuChevronRight,
LuChevronUp,
} from '@qwikest/icons/lucide';
const styledCarouselContextId = createContextId<{
orientation: 'horizontal' | 'vertical';
progress?: Signal<number>;
}>('styled-carousel-context');
const Provider = component$<{
orientation?: 'horizontal' | 'vertical';
}>(({ orientation = 'horizontal' }) => {
const context = {
orientation,
};
useContextProvider(styledCarouselContextId, context);
return <Slot />;
});
const Root = ({
orientation = 'horizontal',
...props
}: PropsOf<typeof HCarousel.Root> & {
orientation?: 'horizontal' | 'vertical';
progress?: Signal<number>;
}) => {
return (
<Provider orientation={orientation}>
<HCarousel.Root
{...props}
orientation={orientation}
slideComponent={Slide}
bulletComponent={Bullet}
titleComponent={Title}
stepComponent={Step}
class={cn('relative', props.class)}
>
{props.children}
</HCarousel.Root>
</Provider>
);
};
const Scroller = component$<PropsOf<typeof HCarousel.Scroller>>(({ ...props }) => {
const context = useContext(styledCarouselContextId);
return (
<HCarousel.Scroller
{...props}
class={cn('flex', context.orientation === 'horizontal' ? '-ml-4' : '-mt-4')}
>
<Slot />
</HCarousel.Scroller>
);
});
const Slide = component$<PropsOf<typeof HCarousel.Slide>>(({ ...props }) => {
const context = useContext(styledCarouselContextId);
return (
<HCarousel.Slide
{...props}
class={cn(context.orientation === 'horizontal' ? 'pl-4' : 'pt-4')}
>
<Slot />
</HCarousel.Slide>
);
});
const Previous = component$<
PropsOf<typeof HCarousel.Previous> & VariantProps<typeof buttonVariants>
>(({ look = 'ghost', size = 'icon', ...props }) => {
const context = useContext(styledCarouselContextId);
return (
<div
class={cn(
'absolute',
context.orientation === 'horizontal'
? '-left-16 top-1/2 -translate-y-1/2'
: '-top-16 right-1/2 translate-x-1/2',
)}
>
<HCarousel.Previous
{...props}
class={cn(buttonVariants({ look, size }), 'group rounded-full', props.class)}
>
<div
class={cn(
context.orientation === 'horizontal'
? 'group-hover:-translate-x-px'
: 'group-hover:-translate-y-px',
'group-hover:transition-all group-hover:duration-300',
)}
>
{context.orientation === 'horizontal' ? (
<LuChevronLeft class="size-10" />
) : (
<LuChevronUp class="size-10" />
)}
</div>
</HCarousel.Previous>
</div>
);
});
const Next = component$<
PropsOf<typeof HCarousel.Next> & VariantProps<typeof buttonVariants>
>(({ look = 'ghost', size = 'icon', ...props }) => {
const context = useContext(styledCarouselContextId);
return (
<div
class={cn(
'absolute',
context.orientation === 'horizontal'
? '-right-16 top-1/2 -translate-y-1/2'
: '-bottom-16 right-1/2 translate-x-1/2',
)}
>
{/* moves content to the right on hover */}
<HCarousel.Next
{...props}
class={cn(buttonVariants({ look, size }), 'group rounded-full', props.class)}
>
<div
class={cn(
context.orientation === 'horizontal'
? 'group-hover:translate-x-px'
: 'group-hover:translate-y-px',
'group-hover:transition-all group-hover:duration-300',
)}
>
{context.orientation === 'horizontal' ? (
<LuChevronRight class="size-10" />
) : (
<LuChevronDown class="size-10" />
)}
</div>
</HCarousel.Next>
</div>
);
});
const Pagination = component$(({ ...props }: PropsOf<typeof HCarousel.Pagination>) => {
return (
<HCarousel.Pagination
{...props}
class={cn('absolute -bottom-10 flex w-full justify-center gap-4', props.class)}
>
<Slot />
</HCarousel.Pagination>
);
});
const Bullet = component$((props: PropsOf<typeof HCarousel.Bullet>) => {
return (
<HCarousel.Bullet
{...props}
class={cn(
'size-5 rounded-full border border-background bg-muted outline-0 outline-muted transition-all duration-300 ease-in-out hover:border-muted',
'data-[active]:border-primary data-[active]:bg-primary data-[active]:text-primary-foreground',
props.class,
)}
/>
);
});
const Title = component$((props: PropsOf<typeof HCarousel.Title>) => {
return (
<HCarousel.Title {...props}>
<Slot />
</HCarousel.Title>
);
});
const Stepper = (props: PropsOf<typeof HCarousel.Stepper>) => {
return (
<HCarousel.Stepper
{...props}
class={cn('flex w-full items-center justify-between', props.class)}
>
{props.children}
</HCarousel.Stepper>
);
};
const Step = component$((props: PropsOf<typeof HCarousel.Step>) => {
return (
<HCarousel.Step
{...props}
class={cn(
'flex items-center gap-1 [&[data-current]_span:first-child]:outline-2 [&[data-current]_span:first-child]:outline-offset-[-2px] [&[data-current]_span:first-child]:outline-primary',
props.class,
)}
>
<Slot />
</HCarousel.Step>
);
});
export const Carousel = {
Root,
Scroller,
Slide,
Previous,
Next,
Pagination,
Bullet,
Title,
Stepper,
Step,
};
Examples
Hero Vertical
1
2
3
4
5
import { component$ } from '@builder.io/qwik';
import { Card, Carousel } from '@qwik-ui/styled';
import { LuChevronDown } from '@qwikest/icons/lucide';
import { LuChevronUp } from '@qwikest/icons/lucide';
export default component$(() => {
return (
<Carousel.Root
class="w-full max-w-sm"
orientation="vertical"
maxSlideHeight={360}
slidesPerView={3}
>
<Carousel.Scroller class="m-1">
{Array.from({ length: 5 }).map((_, index) => (
<Carousel.Slide key={index}>
<div class="p-1">
<Card.Root>
<Card.Content class="flex w-full items-center justify-center pt-6">
<span class="text-3xl font-semibold">{index + 1}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Slide>
))}
</Carousel.Scroller>
<Carousel.Previous>
<LuChevronUp class="size-10" />
</Carousel.Previous>
<Carousel.Next>
<LuChevronDown class="size-10" />
</Carousel.Next>
</Carousel.Root>
);
});
Pagination
1
2
3
4
5
import { component$ } from '@builder.io/qwik';
import { Card, Carousel } from '@qwik-ui/styled';
export default component$(() => {
return (
<Carousel.Root class="max-w-xs">
<Carousel.Scroller>
{Array.from({ length: 5 }).map((_, index) => (
<Carousel.Slide key={index}>
<div class="p-1">
<Card.Root>
<Card.Content class="flex aspect-square items-center justify-center">
<span class="text-4xl font-semibold">{index + 1}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Slide>
))}
</Carousel.Scroller>
<Carousel.Pagination>
<Carousel.Bullet />
<Carousel.Bullet />
<Carousel.Bullet />
<Carousel.Bullet />
<Carousel.Bullet />
</Carousel.Pagination>
</Carousel.Root>
);
});
Stepper
1
2
3
import { component$, useComputed$, useSignal } from '@builder.io/qwik';
import { Card, Carousel, Separator } from '@qwik-ui/styled';
import { cn } from '@qwik-ui/utils';
export default component$(() => {
const steps = ['Address', 'Shipping', 'Payment'];
const progress = useSignal(0);
const progressIndex = useComputed$(() => Math.floor(progress.value / 50));
return (
<Carousel.Root class="w-full" bind:progress={progress}>
<Carousel.Stepper class="pl-4">
{steps.map((title, index) => (
<>
<Carousel.Step key={index}>
<span
class={cn(
'flex size-8 items-center justify-center rounded-full bg-muted text-xl font-semibold',
progressIndex.value >= index && 'bg-primary text-primary-foreground',
)}
>
{index + 1}
</span>
<span class="font-semibold">{title}</span>
</Carousel.Step>
{index < steps.length - 1 && (
<div class="mx-2 flex-grow">
<Separator
class={cn(progressIndex.value > index ? 'bg-foreground' : 'bg-muted')}
/>
</div>
)}
</>
))}
</Carousel.Stepper>
{Array.from({ length: 3 }).map((_, index) => (
<Carousel.Slide key={index}>
<div class="p-1">
<Card.Root>
<Card.Content class="flex h-40 w-full items-center justify-center">
<span class="text-4xl font-semibold">{index + 1}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Slide>
))}
</Carousel.Root>
);
});