にゅるっとした動きのものがなかったので作った。
Next.jsで動くのを目的に作ったので、styled-jsx
を利用している。
SegmentControl.tsx
import css from 'styled-jsx/css' import { useState, useEffect } from 'react' const styles = css` .controls { display: inline-flex; justify-content: space-between; background: #e7e7e7; border-radius: 8px; overflow: hidden; position: relative; width: 100%; } .controls::before { content: ''; background: #5465ff; border-radius: 8px; position: absolute; top: 0px; bottom: 0px; left: 0; transition: transform 0.3s ease, width 0.3s ease; } .segment { color: gray; position: relative; text-align: center; } .segment.active { color: #fff; } label { cursor: pointer; display: block; padding: 5px 0px; transition: color 0.5s ease; } input { opacity: 0; margin: 0; top: 0; right: 0; bottom: 0; left: 0; position: absolute; cursor: pointer; width: 100%; height: 100%; } ` interface Props { segments: { label: string value: string }[] callback: (value: string, index: number) => void defaultIndex?: number } export const SegmentedControl: React.FC<Props> = ({ segments, callback, defaultIndex = 0, }) => { const [activeIndex, setActiveIndex] = useState(defaultIndex) const [segmentLeft, setSegmentLeft] = useState(0) const segmentWidth = `${100 / segments.length}%` useEffect(() => { const id = `${activeIndex}-${segments[activeIndex].value}` const segment = document.getElementById(id) const { width } = segment.getBoundingClientRect() setSegmentLeft(width * activeIndex) }, [activeIndex]) const onChange = (value, index) => { setActiveIndex(index) callback(value, index) } return ( <div className="controls"> {segments.map((item, i) => { return ( <div id={`${i}-${item.value}`} key={item.value} className={`segment ${i === activeIndex && 'active'}`} > <input type="radio" value={item.value} onChange={() => onChange(item.value, i)} checked={i === activeIndex} /> <label htmlFor={item.label}>{item.label}</label> </div> ) })} <style jsx>{styles}</style> <style jsx>{` .segment { min-width: ${segmentWidth}; } .controls::before { width: ${segmentWidth}; transform: translateX(${segmentLeft}px); } `}</style> </div> ) }
呼び出す側
import { SegmentedControl } from './SegmentControl' const IndexPage = () => { const onSegmentChange = (value, index) => { console.log(value, index) } return ( <div style={{ width: '20%', padding: '20px' }}> <SegmentedControl callback={onSegmentChange} defaultIndex={0} segments={[ { label: 'First', value: 'first', }, { label: 'Second', value: 'second', }, ]} /> </div> ) } export default IndexPage
めちゃくちゃ参考にさせていただいたサイト
https://letsbuildui.dev/articles/building-a-segmented-control-component