Curved Tab Bar
November 2024
TSX
import { motion } from 'framer-motion';
import React, { useState } from 'react';
interface IconProps {
className?: string;
}
const HomeIcon = (props: IconProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={props.className}
>
<g fill="none" stroke="white" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5">
<path d="M6.133 21C4.955 21 4 20.02 4 18.81v-8.802c0-.665.295-1.295.8-1.71l5.867-4.818a2.09 2.09 0 0 1 2.666 0l5.866 4.818c.506.415.801 1.045.801 1.71v8.802c0 1.21-.955 2.19-2.133 2.19z" />
<path d="M9.5 21v-5.5a2 2 0 0 1 2-2h1a2 2 0 0 1 2 2V21" />
</g>
</svg>
);
}
const GearIcon = (props: IconProps) => {
return (
<svg className={props.className} xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<g fill="none" stroke="white" strokeWidth="1.5">
<circle cx="12" cy="12" r="3" />
<path d="M13.765 2.152C13.398 2 12.932 2 12 2s-1.398 0-1.765.152a2 2 0 0 0-1.083 1.083c-.092.223-.129.484-.143.863a1.62 1.62 0 0 1-.79 1.353a1.62 1.62 0 0 1-1.567.008c-.336-.178-.579-.276-.82-.308a2 2 0 0 0-1.478.396C4.04 5.79 3.806 6.193 3.34 7s-.7 1.21-.751 1.605a2 2 0 0 0 .396 1.479c.148.192.355.353.676.555c.473.297.777.803.777 1.361s-.304 1.064-.777 1.36c-.321.203-.529.364-.676.556a2 2 0 0 0-.396 1.479c.052.394.285.798.75 1.605c.467.807.7 1.21 1.015 1.453a2 2 0 0 0 1.479.396c.24-.032.483-.13.819-.308a1.62 1.62 0 0 1 1.567.008c.483.28.77.795.79 1.353c.014.38.05.64.143.863a2 2 0 0 0 1.083 1.083C10.602 22 11.068 22 12 22s1.398 0 1.765-.152a2 2 0 0 0 1.083-1.083c.092-.223.129-.483.143-.863c.02-.558.307-1.074.79-1.353a1.62 1.62 0 0 1 1.567-.008c.336.178.579.276.819.308a2 2 0 0 0 1.479-.396c.315-.242.548-.646 1.014-1.453s.7-1.21.751-1.605a2 2 0 0 0-.396-1.479c-.148-.192-.355-.353-.676-.555A1.62 1.62 0 0 1 19.562 12c0-.558.304-1.064.777-1.36c.321-.203.529-.364.676-.556a2 2 0 0 0 .396-1.479c-.052-.394-.285-.798-.75-1.605c-.467-.807-.7-1.21-1.015-1.453a2 2 0 0 0-1.479-.396c-.24.032-.483.13-.82.308a1.62 1.62 0 0 1-1.566-.008a1.62 1.62 0 0 1-.79-1.353c-.014-.38-.05-.64-.143-.863a2 2 0 0 0-1.083-1.083Z" />
</g>
</svg>
)
}
const MagnifyingGlassIcon = (props: IconProps) => {
return (
<svg className={props.className} xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="none" stroke="white" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M19 11.5a7.5 7.5 0 1 1-15 0a7.5 7.5 0 0 1 15 0m-2.107 5.42l3.08 3.08" />
</svg>
)
}
const MessagesIcon = (props: IconProps) => {
return (
<svg className={props.className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" stroke="white">
<rect width="16" height="12" x="4" y="6" rx="2" />
<path d="m4 9l7.106 3.553a2 2 0 0 0 1.788 0L20 9" />
</g>
</svg>
)
}
const HeartIcon = (props: IconProps) => {
return (
<svg className={props.className} xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="white" fillRule="evenodd" d="M5.624 4.424C3.965 5.182 2.75 6.986 2.75 9.137c0 2.197.9 3.891 2.188 5.343c1.063 1.196 2.349 2.188 3.603 3.154q.448.345.885.688c.526.415.995.778 1.448 1.043s.816.385 1.126.385s.674-.12 1.126-.385c.453-.265.922-.628 1.448-1.043q.437-.344.885-.687c1.254-.968 2.54-1.959 3.603-3.155c1.289-1.452 2.188-3.146 2.188-5.343c0-2.15-1.215-3.955-2.874-4.713c-1.612-.737-3.778-.542-5.836 1.597a.75.75 0 0 1-1.08 0C9.402 3.882 7.236 3.687 5.624 4.424M12 4.46C9.688 2.39 7.099 2.1 5 3.059C2.786 4.074 1.25 6.426 1.25 9.138c0 2.665 1.11 4.699 2.567 6.339c1.166 1.313 2.593 2.412 3.854 3.382q.43.33.826.642c.513.404 1.063.834 1.62 1.16s1.193.59 1.883.59s1.326-.265 1.883-.59c.558-.326 1.107-.756 1.62-1.16q.396-.312.826-.642c1.26-.97 2.688-2.07 3.854-3.382c1.457-1.64 2.567-3.674 2.567-6.339c0-2.712-1.535-5.064-3.75-6.077c-2.099-.96-4.688-.67-7 1.399" clipRule="evenodd" />
</svg>
)
}
const TABS = [
{ icon: HomeIcon, label: 'Home' },
{ icon: GearIcon, label: 'Settings' },
{ icon: MagnifyingGlassIcon, label: 'Search' },
{ icon: MessagesIcon, label: 'Messages' },
{ icon: HeartIcon, label: 'Favorites' },
];
const linePath = 'M0,40 Q172,10 344,40';
const motionPath = `path("M0,40 Q172,10 344,40")`;
const motionHorizontalPadding = 12;
export default function CurvedTabBar() {
const [activeTab, setActiveTab] = useState(1);
const offsetDistance = `${motionHorizontalPadding +
(activeTab / (TABS.length - 1)) * (100 - motionHorizontalPadding * 2)
}%`;
return (
<div className="relative flex h-[600px] w-full flex-col items-center justify-center overflow-hidden">
<div
className="relative -mt-[150%] w-full overflow-hidden bg-background sm:-mt-[100%]"
style={{
height: '704px',
borderRadius: '54px',
width: '344px',
boxShadow: '0 0 0 12px #000',
}}
>
<div className="absolute bottom-0 flex h-[100px] w-full flex-col justify-between overflow-hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
width="344"
height="50"
preserveAspectRatio="none"
viewBox="0 0 344 50"
>
<path
d={linePath}
fill="none"
className="stroke-white/10"
strokeWidth="1"
/>
</svg>
<motion.div
style={{
width: '40px',
height: '4px',
borderRadius: '4px',
position: 'absolute',
left: 0,
bottom: 'calc(50% - 4px)',
background: 'white',
boxShadow: 'rgba(255, 255, 255, 0.4) 0px 0px 20px 10px',
zIndex: 10,
offsetPath: motionPath,
}}
initial={{
offsetDistance: '0%',
}}
animate={{
offsetDistance,
}}
transition={{
duration: 0.6,
ease: [0.32, 0.72, 0, 1],
}}
/>
{TABS.map((tab, index) => {
const tabDistance = `${motionHorizontalPadding +
(index / (TABS.length - 1)) * (100 - motionHorizontalPadding * 2)
}%`;
return (
<button
key={index}
className="absolute p-5"
style={{
offsetPath: motionPath,
offsetDistance: tabDistance,
top: '28%',
}}
onClick={() => setActiveTab(index)}
>
<tab.icon className="h-5 w-5 text-primary-dark-12" />
</button>
);
})}
<div
className="pointer-events-none relative z-10 overflow-hidden"
style={{
filter: 'drop-shadow(rgba(0,0,0, 0.5) 0px 3px 10px)',
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="344"
height="50"
preserveAspectRatio="none"
viewBox="0 0 344 50"
>
<path
d="M 0 40 Q 172 10 344 40 V 68 L 0 68 L 0 40"
className="fill-background"
/>
</svg>
</div>
</div>
</div>
</div>
);
}