DocsTab List
Tab List
Animated Tab List Menu - React.js/TailwindCSS ONLY
Installation
No deps needed. Pure React.js/TailwindCSS
import { cn } from '@/utils/cn'
import React, { useEffect, useRef, useState } from 'react'
import { twMerge } from 'tailwind-merge'
//you handle routing logic. Code is not complex, just play with it and you gonna figure out how it works.
export const NavBar: React.FC<{ tabs: string[] }> = ({ tabs }) => {
const fired = useRef(false)
const defaultSelectedTabIndex = 0
const [currentLink, setCurrentLink] = useState<{
index: number
left: undefined | number
width: undefined | number
}>({
index: defaultSelectedTabIndex,
left: undefined,
width: undefined
})
/**
* TailwindCSS scans your codebase and based on that generates styles
* TailwindCSS does not allow to concatenate class names, so just wrote down all possible combinations (you can add more if you need, you got the idea)
* read https://tailwindcss.com/docs/content-configuration#dynamic-class-names
* you can not do like this - `[&:nth-child(${child})]:bg-neutral-950` it won't work
*/
const defaultSelectedTabStyles = [
'[&:nth-child(1)]:dark:bg-white [&:nth-child(1)]:bg-neutral-950',
'[&:nth-child(2)]:dark:bg-white [&:nth-child(2)]:bg-neutral-950',
'[&:nth-child(3)]:dark:bg-white [&:nth-child(3)]:bg-neutral-950',
'[&:nth-child(4)]:dark:bg-white [&:nth-child(4)]:bg-neutral-950'
]
useEffect(() => {
setCurrentLink(() => ({
left: document.getElementById('uuu-btn-' + defaultSelectedTabIndex)?.offsetLeft,
width: document.getElementById('uuu-btn-' + defaultSelectedTabIndex)?.getBoundingClientRect()
.width,
index: defaultSelectedTabIndex
}))
}, [])
return (
<div
className={
'w-fit relative border dark:border-neutral-800 border-neutral-300 rounded-full flex gap-5 items-center justify-center p-2 backdrop-blur-2xl'
}
>
{tabs.map((link, i) => (
<button
key={i}
id={'uuu-btn-' + i}
onClick={() => {
fired.current = true
setCurrentLink(() => ({
left: document.getElementById('uuu-btn-' + i)?.offsetLeft,
width: document.getElementById('uuu-btn-' + i)?.getBoundingClientRect().width,
index: i
}))
}}
className={twMerge(
'transition-colors duration-200 flex items-center justify-center rounded-full h-fit px-2 py-2 text-nowrap',
currentLink.index === i && 'dark:text-neutral-900 text-white',
fired.current ? '' : defaultSelectedTabStyles[defaultSelectedTabIndex]
)}
>
{link}
</button>
))}
<div className={'absolute inset-0 h-full p-2 -z-[1] overflow-hidden'}>
<div className={'relative h-full w-full overflow-hidden'}>
<div
style={{
left: `calc(${currentLink.left || 0}px - 0.75rem + 0.25rem)`,
width: `${currentLink.width || 0}px`
}}
className={twMerge(
`transition-[color,left,width] duration-300 absolute top-1/2 -translate-y-1/2 h-full rounded-full -z-[1]`,
//just skips animation on page load
fired.current ? 'dark:bg-white bg-neutral-950' : 'bg-transparent'
)}
/>
</div>
</div>
</div>
)
}