question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Joy] Add Carousel component

See original GitHub issue

Duplicates

  • I have searched the existing issues

Latest version

  • I have tested the latest version

Summary 💡

@siriwatknp @danilo-leal

So standard Material UI doesn’t provide any carousel component out of the box, however after playing around with the new Joy UI components I started writing a CSS snap carousel component that could possibly be useful for Joy. The idea partially stems from the Google news feed and a few other apps I commonly use.

I’ll provide the src files below as this isn’t a ready component in any way, it still doesn’t have the forward back scroll functions in place, a lot of the type names need to be fixed, and I’m unsure if the virtual library I use is appropriate for this. It definitely needs a bit of work but I’d be really happy to hear people’s thoughts on how a carousel component should be designed.

BUGS So far the only bug I’ve run into is when you scroll to the last element, the first element in the list beings to render, then disappear in a loop. I honestly don’t know if virtual makes sense for this, however major carousel libraries have this kind of support and there are cases where it could be needed. Perhaps you are rendering a gallery then it could make sense to have, images that wouldn’t all be loaded at once, etc.

NOTES There is also a prop called isSSR to switch between standard rendering and using the virtual hook (@tanstack/react-virtual), I still need to devise a solution to allow users to programmatically scroll regardless of the method used.

(Also the file structure is slightly dif from MUI. Would be fixed later.) Index

Carousel Root w/ Virtual Hook

import { mergeRefs } from '@fox-dls/utils';
import { unstable_composeClasses as composeClasses } from '@mui/base';
import { Box, Button, useThemeProps } from '@mui/material';
import { OverridableComponent } from '@mui/types';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import * as React from 'react';
import { CarouselContext } from './context';
import { CarouselRoot } from './styles';
import {
  getCarouselUtilityClass,
  CarouselProps,
  CarouselTypeMap
} from './types';
import { useVirtualizer } from '@tanstack/react-virtual';

const useUtilityClasses = (ownerState: CarouselProps) => {
  const slots = {
    root: []
  };

  return composeClasses(slots, getCarouselUtilityClass, {});
};

export const Carousel = React.forwardRef(function Carousel(inProps, ref) {
  const props = useThemeProps({
    props: inProps,
    name: 'MuiCarousel'
  });

  const {
    className,
    //@ts-ignore
    component = 'div',
    children,
    visibleItems,
    gap,
    isControls,
    vertical,
    viewConfig,
    placeholder,
    isSSR,
    ...other
  } = props;

  const ownerState = {
    ...props,
    component
  };

  const classes = useUtilityClasses(ownerState);

  const scrollerRef = React.useRef();

  const scrollerVirtualizer = useVirtualizer({
    horizontal: vertical ? false : true,
    count: React.Children.count(children),
    getScrollElement: () => scrollerRef.current,
    estimateSize: () => 0,
    ...viewConfig
  });

  return (
    <CarouselContext.Provider value={null}>
      <CarouselRoot
        as={component}
        ownerState={ownerState}
        className={clsx(classes.root, className)}
        ref={mergeRefs([ref, scrollerRef])}
        {...other}
      >
        {isSSR
          ? scrollerVirtualizer.getVirtualItems().map(virtual => (
              <Box key={virtual.index} ref={virtual.measureElement}>
                {placeholder
                  ? React.Children.toArray(children)[virtual.index] ??
                    placeholder
                  : React.Children.toArray(children)[virtual.index]}
              </Box>
            ))
          : React.Children.map(children, (child, index) => {
              if (!React.isValidElement(child)) {
                return child;
              }
              if (index === 0) {
                return React.cloneElement(child, {
                  'carousel-first-child': ''
                });
              }
              if (index === React.Children.count(children) - 1) {
                return React.cloneElement(child, { 'carousel-last-child': '' });
              }
              return child;
            })}
      </CarouselRoot>
      <button onClick={() => scrollerVirtualizer.scrollToIndex(4)}>
        Scroll To Index 2
      </button>
    </CarouselContext.Provider>
  );
}) as OverridableComponent<CarouselTypeMap>;

Carousel.propTypes /* remove-proptypes */ = {
  /**
   * Used to render icon or text elements inside the Carousel if `src` is not set.
   * This can be an element, or just a string.
   */
  children: PropTypes.node,
  /**
   * @ignore
   */
  className: PropTypes.string,
  /**
   * The component used for the root node.
   * Either a string to use a HTML element or a component.
   */
  component: PropTypes.elementType,
  /**
   * The number of items visible at a given time. Defaults to 1
   */
  visibleItems: PropTypes.number,
  /**
   * Item peek in px
   */
  peek: PropTypes.string,
  /**
   * The gap between items in px. Defaults to 0
   */
  gap: PropTypes.string,
  /**
   * Is Verical or Horizontal
   */
  vertical: PropTypes.bool,
  /**
   * Config for view observer
   */
  viewConfig: PropTypes.object,
  /**
   * Children props
   */
  itemProps: PropTypes.shape({}),
  /**
   * Placeholder node to load
   */
  placeholder: PropTypes.node,
  /**
   * Is SSR
   */
  isSSR: PropTypes.bool,
  sx: PropTypes.oneOfType([
    PropTypes.arrayOf(
      PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])
    ),
    PropTypes.func,
    PropTypes.object
  ])
} as any;

Types & Classes

Types & Classes

import * as React from 'react';
import { OverridableStringUnion, OverrideProps } from '@mui/types';
import { SxProps } from '@mui/system';
import { generateUtilityClass, generateUtilityClasses } from '@mui/base';

export type CarouselSlot = 'root';

export interface CarouselTypeMap<P = {}, D extends React.ElementType = 'div'> {
  props: P & {
    /**
     * Used to render icon or text elements inside the Carousel if `src` is not set.
     * This can be an element, or just a string.
     */
    children?: React.ReactNode;
    /**
     * The system prop that allows defining system overrides as well as additional CSS styles.
     */
    sx?: SxProps;
    /**
     * The number of items visible at a given time. Defaults to 1
     */
    visibleItems?: number;
    /**
     * Item peek in px
     */
    peek?: string;
    /**
     * The gap between items in px. Defaults to 0
     */
    gap?: string;
    /**
     * Are the overlay controls available
     */
    isControls?: boolean;
    /**
     * Is Horizontal or Vertical
     */
    vertical?: boolean;
    /**
     * Config for view observer
     */
    viewConfig?: object;
    /**
     * Placeholder node to load
     */
    placeholder?: React.ReactNode;
    /**
     * Is SSR
     */
    isSSR?: boolean;
  };
  defaultComponent: D;
}

export type CarouselProps<
  D extends React.ElementType = CarouselTypeMap['defaultComponent'],
  P = { component?: React.ElementType }
> = OverrideProps<CarouselTypeMap<P, D>, D>;

export interface CarouselClasses {
  /** Styles applied to the root element. */
  root: string;
}

export type CarouselClassKey = keyof CarouselClasses;

export function getCarouselUtilityClass(slot: string): string {
  return generateUtilityClass('MuiCarousel', slot);
}

const CarouselClasses: CarouselClasses = generateUtilityClasses('MuiCarousel', [
  'root'
]);

export default CarouselClasses;

Styles

Carousel Style

import { styled } from '@mui/system';
import { CarouselProps } from '../types';
import { resolveSxValue } from '../../theme/mui/utils';

export const CarouselRoot = styled('div', {
  name: 'MuiCarousel',
  slot: 'Root',
  overridesResolver: (props, styles) => styles.root
})<{ theme?: any; ownerState: CarouselProps }>(({ theme, ownerState }) => [
  {
    '--Carousel-contain': 'layout paint size style',
    '--Carousel-overflow': 'visible hidden',
    '--Carousel-content-visibility': 'unset',
    '--Carousel-controls-z-index': 1,
    '--Carousel-gap': ownerState.gap ?? '0px',
    '--Carousel-height': '100%',
    '--Carousel-peek': ownerState.peek ?? '0px',
    '--Carousel-scrollsnapstop': 'always',
    '--Carousel-visible-items': ownerState.visibleItems ?? 1,

    '&::-webkit-scrollbar': {
      display: 'none'
    },

    '& > *': {
      scrollSnapAlign: 'start'
    },

    ['@supports (-webkit-scroll-behavior:smooth) or (-moz-scroll-behavior:smooth) or (-ms-scroll-behavior:smooth) or (scroll-behavior:smooth)']:
      {
        scrollSnapType: 'var(--Carousel-scroll-snap-type,inline mandatory);'
      },
    margin: '0px !important',
    width: '100% !important',
    height: 'var(--Carousel-height)',
    contain: 'var(--Carousel-contain)',
    containIntrinsicSize: 'var(--Carousel-contain-intrinsic)',
    //contentVisibility: 'var(--Carousel-content-visibility)',
    display: 'grid',
    gridAutoFlow: 'column',
    gridAutoColumns:
      'var( --Carousel-auto-columns, calc( ( 100% - var(--Carousel-gap,16px) * (var(--Carousel-visible-items,unset) - 1) ) / var(--Carousel-visible-items,unset) ) )',
    gap: 'var(--Carousel-gap,16px)',
    gridTemplateRows: 'var(--Carousel-rows,none)',
    justifyContent: 'flex-start',
    minHeight: 'var(--Carousel-min-height)',
    overflow: 'var(--Carousel-overflow,auto hidden)',
    overscrollBehaviorInline: 'contain',
    paddingBlockStart: 'var(--Carousel-padding-block-start,unset)',
    paddingBlockEnd: 'var(--Carousel-padding-block-end,unset)',
    paddingInlineStart:
      'var( --Carousel-padding-inline-start, var(--Carousel-peek,32px) )',
    paddingInlineEnd:
      'var( --Carousel-padding-inline-end, var(--Carousel-peek,32px) )',
    scrollPaddingInline: 'var(--Carousel-peek,32px)',
    touchAction: 'var(--Carousel-touch-action,pan-x pan-y pinch-zoom)',
    scrollbarWidth: 'none'
  }
]);

Examples 🌈

I still need to move all this to its own sandbox.

<AspectRatio ratio="20/19" sx={{ width: '100%', height: '100%' }}>
        <CardContainer>
          <CardContent>
            <Carousel
              gap="0px"
              visibleItems={2}
              gap="10px"
              placeholder={<Loading />}
              isSSR={true}
            >
              <CardContainer sx={{ height: '100%', borderRadius: '0px' }}>
                <CardCover>
                  <img
                    src="https://images.unsplash.com/photo-1656536665880-2cb1ab7d54e3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"
                    alt=""
                  />
                </CardCover>
              </CardContainer>
              <CardContainer sx={{ height: '100%', borderRadius: '0px' }}>
                <CardCover>
                  <img
                    src="https://images.unsplash.com/photo-1656231934649-c476090deb59?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1524&q=80"
                    alt=""
                  />
                </CardCover>
              </CardContainer>
              <CardContainer sx={{ height: '100%', borderRadius: '0px' }}>
                <CardCover>
                  <img
                    src="https://images.unsplash.com/photo-1656501854040-2850f4a37562?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"
                    alt=""
                  />
                </CardCover>
              </CardContainer>
              <CardContainer sx={{ height: '100%', borderRadius: '0px' }}>
                <CardCover>
                  <img
                    src="https://images.unsplash.com/photo-1656501854040-2850f4a37562?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80"
                    alt=""
                  />
                </CardCover>
              </CardContainer>
            </Carousel>
          </CardContent>
        </CardContainer>
      </AspectRatio>

Motivation 🔦

It looks to me as if Joy might be the next big thing for MUI and I would like to attempt to provide useful components so that other developers will be able to make cool things!

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:14
  • Comments:6 (1 by maintainers)

github_iconTop GitHub Comments

3reactions
Rafcincommented, Jul 6, 2022

It should allow for some freedom, with next and previous coming from the hook it’s much easier to implement your own carousel buttons as opposed to having them overlayed on the left and right. If anyone has any feedback I would greatly appreciate it, optimizations, best practices, etc, I would really like to try to get a carousel into Mui so other developers have more in their arsenal!

1reaction
Rafcincommented, Jul 8, 2022

I think this solution is much better, the Carousel object itself is quite simple and honestly lets users decide how they want to handle the navigation aspect. Included is the hook that takes the ref and lets you move forward, back, etc. I’m already making a slightly different hook with better functions but I think the carousel and hook idea might be good. It’s no swiper JS but it’s simple and I think the CSS snap feature is great for most use cases!

Read more comments on GitHub >

github_iconTop Results From Across the Web

React Aspect Ratio component - Joy UI - MUI
The Aspect Ratio component wraps around the content that it resizes. The element to be resized must be the first direct child. The...
Read more >
How does the picture carousel realize the interactive effect of ...
Swipe the last picture to the left to scroll to the first picture. Interactions / On Drag : Add Drag Direction. Vertical/horizontal image...
Read more >
Bootstrap 4 Carousel external controls - Stack Overflow
I was trying to add an external panel (to the carousel component) with links to specific slides. I'm trying to do something like...
Read more >
How to make an Image Carousel - FAQ - Glide Community
Glide can do this. Just create a column for the number of images you want to have in the carousel in the sheet....
Read more >
Carousel Component | Adobe Experience Manager
The Carousel Component allows the content author to present content in a rotating carousel.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found