🎠

Chakra UIにCSSのscroll-snapだけのライブラリなしカルーセルを実装する

2023/02/28に公開

Chakra UIにカルーセルを組み込もうとすると、うまく動かなかったり色々面倒だった。
ちょっとしたカルーセルであればCSSのscroll-snap-系のプロパティを使うことで解決できるので、これを組み込むことを考えた。

なお、今回画像はdog.ceoを利用させていただきた。

まずCSSだとどうなるのか?

元となるCSSでの実装はこのような具合になる。

.slider{
  width: 200px;
  height: 200px;
  overflow-x: scroll;
  align-items: center;
  flex-direction: row;
  scroll-snap-type: x mandatory;  
  display: flex;
}
.slide-img{
  width:  200px;
  height: 200px;
  object-fit: cover;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  aspect-ratio: 1;
}
<div class="slider">
  <img class="slide-img" src="https://images.dog.ceo/breeds/dhole/n02115913_3753.jpg">
  <img class="slide-img" src="https://images.dog.ceo/breeds/corgi-cardigan/n02113186_12793.jpg">
  <img class="slide-img" src="https://images.dog.ceo/breeds/pointer-germanlonghair/hans2.jpg">
</div>

仕組みとしては親コンテナにscroll-snap-type: x mandatoryをつけて、子の要素にscroll-snap-stop: alwaysをつける。ついでにscroll-snap-align: centerも指定している。

これで下記のように簡単なカルーセルができた。

CSSのみで実装する方法は色々と紹介されているので、更に知りたい人は「CSS only carousel」などで検索すると良いだろう

Chakra UIに組み込んでいく

これをChakra UIに組み込むと、下記のようになる。

import { Image, Box, AspectRatio, HStack } from "@chakra-ui/react"
import { FC } from "react"

export const PureCarousel: FC<{ images: string[] }> = ({ images }) => {
  const size = 200
  return <Box>
    <HStack overflowX="scroll"
      w={size}
      sx={{
        scrollSnapType: "x mandatory",
      }}>
      {images.map((img, i) => {
        return <AspectRatio ratio={1} minW={size} sx={{
          scrollSnapAlign: "center",
          scrollSnapStop: "always"
        }}>
          <Image key={i} src={img} w={size} draggable={false} userSelect="none" />
        </AspectRatio>
      })}
    </HStack>
  </Box>
}

sxを利用してscrollSnapTypeのように記載していく。これで同様のカルーセルができる。

応用編: 左右ボタンをつける

ちょっとだけ応用させて左右に移動できるボタンぐらいつけてみる。
スクロールするコンテナにrefを設定し、これをボタンが押されたタイミングで左右に移動させる。

export const PureCarousel: FC<{ images: string[] }> = ({ images }) => {
  const ref = useRef<HTMLDivElement>(null)
  const size = 200
  return <HStack alignItems={"stretch"}>
    <HStack bg="gray.100" w={6} justifyContent="center" cursor={"pointer"} onClick={() => {
      if (!ref.current) return
      console.log(ref.current.scrollLeft)
      ref.current.scrollTo({
        left: ref.current.scrollLeft - size,
      })
      // scrollToPosition(currentPosition - 1)
    }}>
      <Box>◀️</Box>
    </HStack>
    <HStack
      scrollBehavior={"smooth"}
      overflowX="scroll"
      ref={ref}
      w={size}
      sx={{
        scrollSnapType: "x mandatory",
      }}>

      {images.map((img, i) => {
        return <AspectRatio ratio={1} minW={size} sx={{
          scrollSnapAlign: "center",
          scrollSnapStop: "always"
        }}>
          <Image key={i} src={img} w={size} draggable={false} userSelect="none" />
        </AspectRatio>
      })}

    </HStack>
    <HStack bg="gray.100" w={6} justifyContent="center" cursor={"pointer"}
      onClick={() => {
        if (!ref.current) return
        ref.current.scrollTo({
          left: ref.current.scrollLeft + size,
        })
      }}>
      <Box>
        ▶️
      </Box>
    </HStack>
  </HStack>

これで下記のように移動ボタンができた。

GitHubで編集を提案

Discussion