posted: 2019/11/04
React Hooksを利用してCSS Grid上の要素をアニメーションする
CSS Gridは本来
grid-row
やgrid-columns
はアニメーション対象ではなく、Grid上を移動するようなものをそのまま作るのは難しい。今回計算用の疑似要素を配置することでアニメーションさせる方法を見つけたのでメモする。
今回はUpLabsのSmoothBottomBarを再現してみた。
作り方
1. まずは基礎部分用意
最初は素の感じでGridを作る。Iconの利用だけ配列を使っているところだけ若干トリッキーかもしれない
主要なコードとしてはこんな感じ。今回は
react-icons
を利用した。いろんなアイコン盛り合わせでとても良かったimport { FiUser, FiHome, FiInbox } from "react-icons/fi"
export const Menu1 = () => {
const icons = [FiHome, FiInbox, FiUser]
return (
<Container>
<MenuGrid>
{icons.map((Icon) => (
<IconWrap>
<Icon />
</IconWrap>
))}
</MenuGrid>
</Container>
)
}
2. 計算用のCellを追加
次に計算用のCellを追加する。動くとこんな感じになる。
この辺からhooksが出てくる。
まず先程のiconをGridのCellとして配置するように書き換える。
const Item = styled.div`
${({ x }) => css`
grid-column: ${x};
`}
grid-row: 1;
`
export const Menu2 = () => {
const [gridPosition, setGridPosition] = useState<number>(1)
// ...
{icons.map((Icon, i) => (
<Item x={i + 1} onMouseOver={(e) => setGridPosition(i + 1)}>
<IconWrap>
<Icon />
</IconWrap>
</Item>
))}
// ...
}
そして計算用のGridCellも配置する。今回は
PositionCalcurator
と名付けた。// これもItemを継承しているので、colとrowが設定される
const PositionCalcurator = styled(Item)`
border: 1px solid red; // debug用のborder
`
export const Menu2 = () => {
const [gridPosition, setGridPosition] = useState<number>(1)
const calcuratorRef = useRef<HTMLElement>(null)
const icons = [FiHome, FiInbox, FiUser]
return (
<Container>
{/* ... */}
<PositionCalcurator ref={calcuratorRef} x={gridPosition} />
{/* ... */}
</Container>
)
}
ここまで来るとこんな具合に動く様子が確認できる
3. カーソル部分を追加
計算用のセルにカーソルのセルを追加してこれをアニメーションさせる。
2で配置した計算用refsから今度は
この結果は
また、effectの
useEffect
で計算用のcellの矩形を取得する。この結果は
cursorRect
と名付けたstate
へ格納する。また、effectの
dependency
としてgridPosition
を利用している(が、もしかしたらこれはeslint-plugin-react-hooks
に怒られるかもしれない)export const Menu3 = () => {
const [gridPosition, setGridPosition] = useState<number>(1)
const [cursorRect, setCursor] = useState<null | Rect>(null) // append
const calcuratorRef = useRef<HTMLElement>(null)
useEffect(() => {
if (!calcuratorRef.current) return
const top = calcuratorRef.current.offsetTop
const left = calcuratorRef.current.offsetLeft
const width = calcuratorRef.current.clientWidth
const height = calcuratorRef.current.clientHeight
const cursor = { top, left, width, height }
setCursor(cursor)
}, [gridPosition])
あとはこの矩形を
Cursor
と名付けたコンポーネントに与えて描画させる。position:absolute;
にしたGridから独立した要素なのでアニメーションができるconst Cursor = styled.div`
position: absolute;
transition: 0.4s;
transition-timing-function: ease-in-out;
background: green;
opacity: 0.5;
${({ top, left, width, height }) => css`
top: ${top}px;
left: ${left}px;
width: ${width}px;
height: ${height}px;
`};
`
export const Menu3 = () => {
const [cursorRect, setCursor] = useState<null | Rect>(null) // append
// ...
return (
<Container>
{/* 先頭に置く! */}
{cursorRect && <Cursor {...cursorRect} />}
4. アイコンに文字を出す
ほぼこれで完成に近いが、あとは本家に習って文字を出すのも追加してみる。
こんな具合でanimationさせる要素を追加するだけになる。
hoverの疑似クラスだけだとカーソルと合わなくなるため、
hoverの疑似クラスだけだとカーソルと合わなくなるため、
active
というpropsを利用することにした。export const Menu4 = () => {
const [gridPosition, setGridPosition] = useState<number>(1)
const isActive = useCallback((x) => gridPosition === x, [gridPosition])
const icons = [["Home", FiHome], ["Inbox", FiInbox], ["Profile", FiUser]]
return (
{/* ... */}
{icons.map(([text, Icon], i) => (
<Item x={i + 1} onMouseOver={(e) => setGridPosition(i + 1)}>
<AnimateIcon
x={i + 1}
onMouseOver={(e) => setGridPosition(i + 1)}
text={text}
active={isActive(i + 1)}
>
<Icon />
</AnimateIcon>
</Item>
))}
)
AnimateIcon
周りはこんな感じconst AnimateIconInner = styled(IconWrap)`
transition: 0.5s;
::after {
font-size: 0.6em;
transition: 0.5s;
overflow: hidden;
content: attr(data-text);
${({ active }) => css`
width: ${active ? "100%" : "0px"};
`}
}
`
const AnimationContainer = styled.div`
width: auto;
`
const AnimateIcon = ({ x, onMouseOver, active, children, text }) => {
return (
<Item x={x} onMouseOver={onMouseOver}>
<AnimateIconInner active={active} data-text={text}>
<AnimationContainer>{children}</AnimationContainer>
</AnimateIconInner>
</Item>
)
}
5. 諸々調整する
あとは色々とパラメータを調整したバージョンが下記になる。
まとめ
CSS Gridのアニメーションについて考察してみた。やはりどうしてもhooksに多くの部分を頼ってしまうことにはなるが、Flexなどでやるよりは等間隔であることを保ちやすかったり、素のjavascriptですべて計算するよりはメリットが大きそうな感覚がある。
ちょっと他にも3x3のロックマンっぽいおパターンなども作ってみた。
https://amination-grid-menu.netlify.com/
https://amination-grid-menu.netlify.com/
色々応用も効いて楽しいのでおすすめしたい。