Closed chaance closed 2 months ago
https://react-bootstrap.github.io/components/carousel/
https://brainhubeu.github.io/react-carousel/
https://kenwheeler.github.io/slick/
https://flickity.metafizzy.co/
https://react-responsive-carousel.js.org/
Looking at the above, the spectrum of available features appears to be the following. I've made these checkboxes so we can agree which we would like to support.
Please feel free to and add anything not covered here to the list! Once agreed on these, I'll propose an API π
SlideImage
, SlideDescription
to restrict the types of content? @chaance raises an interesting point below about draggable functionality if the carousel is an open box with draggable things inside it.Great start. Can't believe there's not a slick-carousel
on the list here since that's been the one I historically have seen most indie devs reach for despite being unmaintained for years π
My biggest concern with drag-to-slide is how it works when composed with other draggable/slideable components composed within the carousel. Are nested carousels supported in any case? What about Slider
or audio
/video
element controls inside of a Carousel
? I'm sure we'll get into some funky edge cases here, just something I might consider early if it helps.
In the Usain Bolt example, I imagine these slide previews might overflow at certain widths, and they too would become independently draggable. A carousel inside a carousel. π
Probably worth adding https://flickity.metafizzy.co/ to the list as well. That's arguably one of the best out there I think.
Can't believe there's not a
slick-carousel
on the list here
Your wish is my command π
My biggest concern with drag-to-slide is how it works when composed with other draggable/slideable components composed within the carousel
Yeah I was also wondering what sort of media we think should be allowed in these. Are we happy to say they are an open box or would we expose something like SlideImage
, SlideDescription
to limit the content? π€
Yeah I was also wondering what sort of media we think should be allowed in these. Are we happy to say they are an open box or would we expose something like
SlideImage
,SlideDescription
to limit the content? π€
My thought is that it probably has to be a bit of an open box, but it's potentially problematic even with non-interactive content in ways I haven't considered fully. I'd probably want to still be able to select text with a mouse, for example. I wonder if it would be worth trying to detect the intent of a particular action -- is the user clicking or touching to pressing something, or are they intending to drag? I'd be interested to see how/if React Aria handles this when usePress
and useMove
are both triggered.
Some notes after chatting through the discovery with @jjenzz.
The feature list posted above looks solid.
Two things that require further thinking:
Some notes to clarify particular features:
Prev/Next Buttons
Thumbails Thumbnails would just be content inside the indicator button. The component would render a button by default, and the consumer just adds content inside.
Decouple events from scrolling You can set the amount of slides per pane, which are scrolled when you click a next/prev button. However, the touch/swipe/trackpad interaction should not necessarily be tied to this setting.
It works well for image gallery carousels, the kind you see when you click an image in an image grid on Twitter or FB. But there are many cases where you want the touch/swipe/trackpad interactions to function as a normal scroll area, with only the next/prev and indicator buttons sliding a whole pane.
Some research is required here to figure out exactly how best to manage this.
Option to disable swiping on pointer devices This is something to consider. Instagram and Twitter don't support swiping in their image gallery components. It's arguably a mistake, and I'd vote that it is a mistake.
Indicators We have identified three different types of indicator element.
<button>
by default, and could optionally be rendered as a <div>
. Sometimes they indicate the total panes count, others indicate the total slides count. This should be verified by research though, I'm now thinking they only indicate slides when they're progress bars, which would mean we may not need to support both behaviours on this component.isScrolling state Expose the state, so consumers can animate/highlight something (like an indicator or scrollbar) while the carousel is scrolling.
Autoplay
Cursor key navigation Cursor keys should navigate slides, even if there is no focusable content inside the slides. Like an image gallery carousel.
Many carousels have focusable content in each slide. For example, focusable cards on Deliveroo, focusable cards on Netflix, tabs in a carousel on Youtube etc. So we need to ensure that, for example, cursor key navigation still works on tabs, even though each tab would be inside an element like <CarouselItem>
.
Nested carousels
I really really like where this is going! β₯οΈ
You can set the amount of slides per pane
There is something interesting related to this in Flickity which I haven't seen many libs support and we haven't mentioned here: variable width cells (ie. images of differents widths/ratios for example).
This is kinda important I would say, and does change the way things will have to be calculated probably (especially around swiping, etc).
Indicators We have identified three different types of indicator element.
Indicator Button would render a
variable width cells
I didn't think of this, so we should probably look into it a little. I see some carousel libs support it, but off the top of my head, I can't think of any valid use cases or any existing examples in the wild. It seems like it would always be a bad design.
but this seems to mention other stuff like number of panes, number of slides,
On Netflix in the screenshot posted above, the indicators reflect the number of panes/pages available, not the number of total slides.
In other carousels, like social media stories (Fleets, IG Stories, FB Stories etc.), the indicators reflect the total number of slides/items.
In both of the cases I've just mentioned, the indicators are not interactive. But often, you would want interactive indicators like the little button dots you mentioned.
variable width cells
I didn't think of this, so we should probably look into it a little. I see some carousel libs support it, but off the top of my head, I can't think of any valid use cases or any existing examples in the wild. It seems like it would always be a bad design.
Here's one of the flickity examples: https://codepen.io/desandro/pen/GgQREP I can definitely see at least this use case for image galleries as you have portrait vs. landscape.
but this seems to mention other stuff like number of panes, number of slides,
On Netflix in the screenshot posted above, the indicators reflect the number of panes/pages available, not the number of total slides.
In other carousels, like social media stories (Fleets, IG Stories, FB Stories etc.), the indicators reflect the total number of slides/items.
In both of the cases I've just mentioned, the indicators are not interactive. But often, you would want interactive indicators like the little button dots you mentioned.
Ah ok I see, I wasn't clear on whether you meant the numbers should actually appear on the page (like 3/5). I see what you're saying now, meaning how many indicators based on if they're indicating slides or panes, gotcha.
I can definitely see at least this use case for image galleries as you have portrait vs. landscape.
I would file this under "bad design". It's passable in this exact case (the full-width carousel), but still bad. The image should be centered horizontally and vertically inside a fixed-width container.
In a typical image gallery, where you're viewing a single image at a time (the kind on Twitter/IG/FB etc.), this would be very bad. You never want the UI (next/prev buttons etc.) around the image to shift. So the container should always be the same size, and the image is centered inside it.
So at this point, I'd still file this under "not important at all", and potentially even "we should actively discourage this".
π€ I don't see anything shifting on this example. But anyway, just making sure we capture all the things out there.
This is still very much in DRAFT, but wanted to jot down progress so far in case there are any opinions on this direction so far:
See bullets below for detailed explanation of various bits.
<Carousel
as="div" // default `div`
slidesPerPage={2} // default `auto`
loop={false} // default `true`?
page={1} // default `undefined`
defaultPage={1} // default `undefined`
onPageChange={page => {}} // default `undefined`
onScroll={event => {}} // default `undefined`
onDragScroll={event => {}} // default `undefined`
>
<CarouselSlide as="div" />
<CarouselSlide as="div" />
<CarouselSlide as="div" />
<CarouselSlide as="div" />
<CarouselSlide as="div" />
<CarouselSlide as="div" />
<CarouselSlide
as="div"
autoFocus // Scrolls into view
/>
{/* non-interactive indicator */}
<CarouselIndicator as="ol">
<CarouselIndicatorItem as="li" />
</CarouselIndicator>
{/* interactive indicator */}
<CarouselIndicator as="ol">
<CarouselIndicatorItem as="li">
<CarouselIndicatorButton as="a" />
</CarouselIndicatorItem>
</CarouselIndicator>
<CarouselButtonPrevious as="button" />
<CarouselButtonNext as="button" />
</Carousel>
CarouselIndicatorItem
and CarouselIndicatorButton
- My thinking here was that the user would render one of these and we would clone it for each page and if they wanted an indicator per slide
then they would do slidesPerPage={1}
. However, this doesn't cover the case where users might want to put their own content inside. I need to think about this part some more (hence DRAFT). I've been thinking about this (but not hugely keen):
// 1. When there is one slide per page (for thumbnail case usually)
<CarouselIndicator as="ol">
<CarouselIndicatorItem as="li"><img alt="thumb" /></CarouselIndicatorItem>
<CarouselIndicatorItem as="li"><img alt="thumb" /></CarouselIndicatorItem>
</CarouselIndicator>
// 2. When you want indicator to clone for you
<CarouselIndicator as="ol">
<CarouselIndicatorItem as="li" />
</CarouselIndicator>
// 3. When there are multiple slides per page and you want custom content
// (this is the one I hate). Do we need this?
<CarouselIndicator as="ol">
{({ page }) => (
<CarouselIndicatorItem as="li">{page}</CarouselIndicatorItem>
)}
</CarouselIndicator>
Perhaps we could stick with 2
for now and then 1
would be part of ImageCarousel
(and 3. can do one π
)?
CarouselIndicatorButton
- Using an anchor tag by default as I plan to get this working without JS as one of our first progressively enhanced SSR components and potentially use aria-current="step"
here.
CarouselButtonPrevious
and CarouselButtonNext
- Prev/next page. If they want to navigate per slide then they would do slidesPerPage={1}
. These would only render client-side. Users can scroll until JS kicks in.
slidesPerPage
- If auto
, a page is considered 100% width of the carousel. To page one slide at a time they can make the carousel the same width as a slide or make slides 100% width and set slidesPerPage
to 1
.
page
- If set to a page that doesn't exist, will show closest page or page % total
if looped.
autoFocus
- The furthest slide in DOM with this attribute will scroll into view.
onPageChange
- Still fires when dragging/scrolling when the page bounds have changed.
onScroll
- Any kind of scroll, trackpad or dragging.
onDragScroll
- Drag scroll only.
A couple of initial thoughts, but otherwise I love the proposed API!
autoFocus
imply that as slides change automatically that the new slide receives focus as it comes into view?
CarouselIndicatorItem
andCarouselIndicatorButton
- My thinking here was that the user would render one of these and we would clone it for each page and if they wanted an indicator perslide
then they would doslidesPerPage={1}
. However, this doesn't cover the case where users might want to put their own content inside. I need to think about this part some more (hence DRAFT). I've been thinking about this (but not hugely keen):
Didn't see this as an option in your post, but what about:
<CarouselIndicator as="ol">
{({ pages }) => pages.map((page) => (
<CarouselIndicatorItem as="li" key={page.id}>
<CarouselIndicatorButton as="button" aria-label={page.title}>
<img alt="" />
</CarouselIndicatorButton>
</CarouselIndicatorItem>
))}
</CarouselIndicator>
I think it might actually be kind of important to support autoplay
Yep, I think we're all agreed we need this but as Colm touched on above, we're thinking that could be the second release of this (which we could work on right away if we wanted to). There are features like autoplay
that appear to be more suited to image carousels. So, we might just build a separate Slideshow
component that would re-compose Carousel
and include things like autoplay and arrow key navigation.
When we come to do that work, perhaps we will realise it should just be part of Carousel
, which is fine. We're just omitting it now to help save time on those discussions (and the added complexity of doing it well like you say) so we can get an MVP version out there sooner.
Does
autoFocus
imply that as slides change automatically that the new slide receives focus as it comes into view?
Nope. I'm imagining it much like autoFocus
on an input. So, when the Carousel
mounts/loads, if a CarouselSlide
has autoFocus
it will gain focus and scrollIntoView
. I added this because the consumer won't necessarily know what page a particular slide is on so they can't just do defaultPage={3}
. Pages could be dependent on viewport dimensions. That's why I also added this:
onPageChange
- Still fires when dragging/scrolling when the page bounds have changed.
So, if they have page={2}
but also have autoFocus
on a slide that isn't on page 2, it would scroll into view and call the onPageChange
callback :+1:
Didn't see this as an option in your post, but what about
That is the same as my proposal I believe except I meant we would do the map under the hood for them. I think I confused the situation by rendering page
in the item. That wasn't meant to be a component, it's just the page number. The focus of the proposal was the function as child thing tho like yours:
<CarouselIndicator>
{({ page }: { page: number }) => (
<CarouselIndicatorItem>
<CarouselIndicatorButton>
Page {page}
</CarouselIndicatorButton>
</CarouselIndicatorItem>
)}
</CarouselIndicator>
There is no page
data object here, only what page number the indicator is trying to render an item for.
I'm just not sure if this pattern is worth it for our first release. This is the sort of thing I'd prefer to pass on personally until we understand how people are using it and it is specifically requested for something that cannot be achieved without it. I'm struggling to think of genuine use-cases that 1
and 2
don't already cover. Can anyone else?
When we come to do that work, perhaps we will realise it should just be part of
Carousel
, which is fine. We're just omitting it now to help save time on those discussions (and the added complexity of doing it well like you say) so we can get an MVP version out there sooner.
π
There is no
page
data object here, only what page number the indicator is trying to render an item for.
Right, I think the difference is that I'm thinking of an API where we register pages in context as slides are rendered. This let's consumers map over that data if they want, or they just render the indicators one at a time, whatever they want. I think I get what you're saying about cloning them so they only have to render once, but I generally like the APIs to be as explicit as possible. But I think generally I agree that:
This is the sort of thing I'd prefer to pass on personally until we understand how people are using it and it is specifically requested for something that cannot be achieved without it.
This let's consumers map over that data if they want
What data are you referring to here? A page doesn't have data as far as I understand it (apart from what page number it is) and neither do slides, they just have children. So, since the indicators render one per page, we can tell it which page number it is being rendered for but I'm not sure what other data there is?
I think I get what you're saying about cloning them
Ah, no, cloning wouldn't happen here. It would only happen in example 2
. With the child as function I would do something like this internally:
function CarouselIndicator({ children }) {
const context = useCarouselContext();
return [...Array(context.pagesCount)].map((_, index) => children({ page: index + 1 }))
}
The consumer doesn't create the pages, we do internally, they're like an invisible concept that only we know about because the number of pages created depends entirely on viewport width, number of slides & slidesPerPage etc. so there isn't really anything for them to "map" over. They don't have titles for example, slides might but slides !== pages
and indicators are for pages.
Tbh, I'm actually starting to wonder if we should just do away with 2
and have 1
and 3
? I like to try to avoid children as function where possible but maybe it is the best approach here. It would cover case 2
and keep things open for whatever crazy stuff people get up to.
Or, maybe we should just do 3
only π¬ It's a consistent API for all cases then π
The consumer doesn't create the pages, we do internally, they're like an invisible concept that only we know about because the number of pages created depends entirely on viewport width, number of slides & slidesPerPage etc. so there isn't really anything for them to "map" over.
This was my assumption, my thought is that by exposing this as data, that's what I'm imagining they could map over to render the indicators. That way they never have to think about how many indicators they need but they also have full control of composition for each indicator that is rendered.
It's very possible that I'm completely misreading the entire concept here somehow, haha. Happy to chat quickly if you want to walk me through it, because in my head what I'm thinking still kind of makes the most sense to me. π
Okay so, @chaance and I just had a quick call. We're on the same page now and have agreed to do the map
thing so indicator would look something like this:
function CarouselIndicator({ children }) {
const context = useCarouselContext();
return children([...Array(context.pagesCount)]);
}
Even though they're just mapping over some page numbers, we decided to go with this anyway because having the map
on the consumer side is more explicit and clearly shows for them that their render is rendering multiple things there.
Also, I'm going to go with this API alone and remove options 1
and 2
as this covers all cases.
Let me know if anyone has any objections π
Catching up with this now, and it's awesome you got to the conclusion I was about to suggest to! β₯οΈ
My main reason for it was that we've been trying to have all parts exposed for people to hook their own events, etc.
Having us clone the indicators would kinda prevent that as users wouldn't be able to for example add an onClick
to a specific indicator.
A few other minor points:
CarouselIndicator
should be CarouselIndicators
(as I understand it's the list containing the indicators)CarouselIndicatorItem
would become CarouselIndicator
too, this way we would have these potential hierarchies:
CarouselIndicators
CarouselIndicator
or:CarouselIndicators
CarouselIndicator
CarouselIndicatorButton
CarouselButtonPrevious
and CarouselButtonNext
could be CarouselPrevious
and CarouselNext
like in Dialog
, we've gone simpler names like DialogClose
, AlertDialogAction
, AlertDialogCancel
, etconDragScroll - Drag scroll only.
What is drag scroll?
Another option would be:
CarouselIndicatorList
CarouselIndicator
This would be inline with TabList
.
It seems odd to me that CarouselIndicatorButton
is nested inside CarouselIndicator
. I would have expected CarouselIndicator
to be the actual
Some more carousels for inspiration.
If you scroll down to "Annotate", you can see a carousel where the indicator buttons actually have the progress indicator around them. This is a common design pattern on marketing sites.
If you scroll down to "Feedback", you'll see a testimonial carousel. As the carousel slides, the first slide shrinks and fades out. Of course this animation would be handled consumer-side, I'm just noting it so we make sure it's possible.
Another option would be:
CarouselIndicatorList CarouselIndicator
This would be inline with
TabList
.
I like that too.
It seems odd to me that
CarouselIndicatorButton
is nested insideCarouselIndicator
. I would have expectedCarouselIndicator
to be the actual , no?
Isn't it so that we can do both interactive and non-interactive indicators?
Agree with all the naming changes. Thanks @benoitgrelard :+1:
It seems odd to me that
CarouselIndicatorButton
is nested insideCarouselIndicator
. I would have expectedCarouselIndicator
to be the actual<button>
, no?
@colmtuite I had originally thought that too but as Benoit has pointed out we need both non-interactive and interactive. So, CarouselIndicator
would be an li
and if you want an interactive one you would put a CarouselIndicatorButton
inside it that would be an a
under the hood and bind all the click logic for you. You would end up with something like this DOM for each:
// non-interactive
<ol>
<li aria-current="step"><span style="visually hidden styles">1</span></li>
<li><span style="visually hidden styles">2</span></li>
</ol>
// interactive
<ol>
<li aria-current="step">
<a href="#slide-id1">
<span style="visually hidden styles">1</span>
</a>
</li>
<li>
<a href="#slide-id2">
<span style="visually hidden styles">2</span>
</a>
</li>
</ol>
We would add the visually hidden number for them if they don't provide their own children for accessibility purposes. Also:
Using an anchor tag by default as I plan to get this working without JS as one of our first progressively enhanced SSR components [...].
What is drag scroll?
Some sliders when you drag slightly they slide the whole pane for you (scroll snap). Others, it behaves the same as dragging a scrollbar (the Google cast example does this, it just scrolls). I was differentiating these two behaviours.
For now we're thinking, just let it scroll and if people want the snap behaviour they can use CSS or control it with whichever animation libs they choose.
Here's a carousel where the progress bar is not attached to the indicator buttons. https://www.jpl.nasa.gov/
On Twitch front page, they have a carousel where the slides themselves act as indicators, sliding the carousel. https://www.twitch.tv/
Any news on when this will ship? π€ Currently using swiperjs and my hands feel dirty
@schonert haha. Right now we're working on Select, Toast, Menubar, and HoverMenu. We'll move back to Carousel after that, though it will likely become ScrollableList (from the Stitches website)
Hi! I saw that Select and Toast is in beta and congrats! Im curious if the Carousel next in line?
Hi again! Any update on this issue :), perhaps anything we could do to help?
Any updates? What can we do to help? I sincerely look forward to this primitive, so as not to connect third-party libraries, and this was already in our favorite RadixUI π
Hey folks, just thought I would add some info here in case it's useful for yall - we're massive fans of Radix UI and we'd love to have it power the carousels for https://replo.app - currently we're using https://splidejs.com/, but wanted to note some shortcomings we've had to work around in case they're useful input:
Hope this helps! Would love to help out any way we can here!
Would be nice to have the possibility to use ScrollTimeline with Web Animation API. Here's a pagination that I implemented that uses this API to provide smooth animation between the indicators' width
and background-color
based on the scroll offset of the scroll area.
https://user-images.githubusercontent.com/995050/201342289-38b52583-3fb4-464c-871e-1f2ae1e26c9f.mp4
Really excited for this π
Hi, are there any updates on this primitive? Is it in development? Because it disappeared from the Radix website as coming soon
.
No, it isn't in development at the moment.
Any updates here?
No, same as above.
Looking forward to this. Any updates, please?
I don't understand why this carousel component was not integrated into the radix-ui primitive, as it is already used on the radix-ui.com website π€
There is already an existing component: https://github.com/radix-ui/website/blob/main/components/marketing/Carousel.tsx
@Vintotan That's weird
@Vintotan tmk, the version on the website is an incomplete version of the accessible carousel we were building that supports the website's specific use-case. carousels come with some complex a11y requirements that we never got around to finishing (especially if it is to be customisable) hence why it was not added to radix yet.
@Vintotan tmk, the version on the website is an incomplete version of the accessible carousel we were building that supports the website's specific use-case. carousels come with some complex a11y requirements that we never got around to finishing (especially if it is to be customisable) hence why it was not added to radix yet.
Can we have a simple version that not so customizable first, then optimize later? Is that anything we can help?
@jjenzz Do you have any update for this? We would really love to see it implemented!
Any update on this issue :)
Also interested in this being added to Radix!
Pretty please, give us a carousel primitive π₯ΊππΌ
@jjenzz Is this something that will be developed, or should we assume any progress has been paused for the rest of the year? Our team needs to know if we need to begin searching outside of the Radix UI primitives for this, or if we can hold off until this is out.
We'd love to hear an update form your end π
I don't think carousels are needed in Radix. Carousels are not a primitive. So I can understand why they don't want to do it. (Also, Radix has no responsibility to your team needing to know anything).
Carousels should be used with care, designers always seem to do the old "can we justβ¦" dance with them, and they get complex quickly. KISS approach with a working 1st version is best.
Instead, use this simple typescript hook with CSS scroll-snap and get it to done.
And in the new Radix UI components that are prestyled?
I don't think carousels are needed in Radix. Carousels are not a primitive. So I can understand why they don't want to do it.
What is and is not a primitive is subjective. Plenty of the "primitives" in Radix are not direct HTML element wrappers like
(Also, Radix has no responsibility to your team needing to know anything).
If they have no plans on implementing this, then they should state so and close this issue. Otherwise, teams are being delayed.
See https://github.com/radix-ui/primitives/discussions/375