I want to make an animated tab like:

I am using React with Tailwind. This is my code:
import React from 'react'
import clsx from 'clsx'
export const Modal = () => {
const [theme, setTheme] = React.useState<'light' | 'dark' | 'system'>('light')
return (
<div className="flex mx-2 mt-2 rounded-md bg-blue-gray-100">
<div
className={clsx('flex-1 py-1 my-2 ml-2 text-center rounded-md', {
'bg-white': theme === 'light',
'transition duration-1000 ease-out transform translate-x-10':
theme !== 'light',
})}
>
<button
className={clsx(
'w-full text-sm cursor-pointer select-none focus:outline-none',
{
'font-bold text-blue-gray-900': theme === 'light',
'text-blue-gray-600': theme !== 'light',
}
)}
onClick={() => {
setTheme('light')
}}
>
Light
</button>
</div>
<div
className={clsx('flex-1 py-1 my-2 ml-2 text-center rounded-md', {
'bg-white': theme === 'dark',
})}
>
<button
className={clsx(
'w-full text-sm cursor-pointer select-none focus:outline-none',
{
'font-bold text-blue-gray-900': theme === 'dark',
'text-blue-gray-600': theme !== 'dark',
}
)}
onClick={() => {
setTheme('dark')
}}
>
Dark
</button>
</div>
<div
className={clsx('flex-1 py-1 my-2 mr-2 text-center rounded-md', {
'bg-white': theme === 'system',
})}
>
<button
className={clsx(
'w-full text-sm cursor-pointer select-none focus:outline-none',
{
'font-bold text-blue-gray-900': theme === 'system',
'text-blue-gray-600': theme !== 'system',
}
)}
onClick={() => {
setTheme('system')
}}
>
System
</button>
</div>
</div>
)
}
But it looks like:

As I use translate-x-10 when the theme is not light, therefore the text moves as well.
I would love to make the UI exactly as the above one while still using buttons for the actual tabs.
Minimal Codesandbox → https://codesandbox.io/s/mobx-theme-change-n1nvg?file=/src/App.tsx
How do I do it?
You can do this animation very easily, you need to add another tag element inside the parent element that holds three buttons.
So, this element will track which button is active, and it will be moving based on their width.
For example, if the first button is active, this element will not be translated at all, because it is the first element, so the position will be 0.
This element that will do the animation stuff will have absolute positioning, like this:
.tab-item-animate {
position: absolute;
top: 6px;
left: 6px;
width: calc(100% - 12px);
height: 32px;
transform-origin: 0 0;
transition: transform 0.25s;
}
First button active:
.tabs .tabs-item:first-child.active ~ .tab-item-animate {
transform: translateX(0) scaleX(0.333);
}
Second button active:
.tabs .tabs-item:nth-child(2).active ~ .tab-item-animate {
transform: translateX(33.333%) scaleX(0.333);
}
Third button active:
.tabs .tabs-item:nth-child(3).active ~ .tab-item-animate {
transform: translateX(33.333% * 2) scaleX(0.333);
}
I don't have so much experience with Tailwind, but I'm not sure if you can manage the whole thing with it (maybe you can do some other manipulations with my code to do it only with Tailwind).
I added a separate CSS file for this, I've provided a demo, based on the code that you've shared:
tabs animated link
PS: I've changed a bit your HTML structure, you don't need to add another div just above each button, it is not necessary.
Turns out, it is possible with pure Tailwind.
module.exports = {
theme: {
extend: {
translate: {
200: '200%',
},
},
},
}
import * as React from "react"
import { observer } from "mobx-react"
import clsx from "clsx"
import { useStore } from "./context"
const AppTheme = observer(() => {
const {
theme: { app },
updateTheme,
} = useStore()
return (
<>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<div className="mt-2">
<h4 className="text-xl font-bold text-gray-800">Background</h4>
</div>
</div>
<div className="relative mx-2 mt-2 rounded-md bg-gray-100">
<div
id="slider"
className={clsx(
"absolute inset-y-0 w-1/3 h-full px-4 py-1 transition-transform transform",
{
"translate-x-0": app === "light",
"translate-x-full": app === "dark",
"translate-x-200": app === "system",
},
)}
style={
app === "system"
? {
transform: "translateX(200%)", // if you added `translate-x-200` to `tailwind.config.js` then you can remove the `style` tag completely
}
: {}
}
>
<div
className={clsx(
"w-full h-full bg-white rounded-md",
{
active: app === "light",
"bg-gray-600": app === "dark",
},
{
// needs to be separate object otherwise dark/light & system keys overlap resulting in a visual bug
["bg-gray-600"]: app === "system",
},
)}
></div>
</div>
<div className="relative flex w-full h-full">
<button
tabIndex={0}
className={clsx(
"py-1 my-2 ml-2 w-1/3 text-sm cursor-pointer select-none focus:outline-none",
{
active: app === "light",
"font-bold text--gray-900": app === "light",
"text--gray-600": app !== "light",
},
)}
onKeyUp={(event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Tab")
updateTheme({
app: "light",
})
}}
onClick={() => {
updateTheme({
app: "light",
})
}}
>
Light
</button>
<button
tabIndex={0}
className={clsx(
"py-1 my-2 ml-2 w-1/3 text-sm cursor-pointer select-none focus:outline-none",
{
active: app === "dark",
"font-bold text-white": app === "dark",
"text--gray-600": app !== "dark",
},
)}
onKeyUp={(event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Tab")
updateTheme({
app: "dark",
})
}}
onClick={() => {
updateTheme({
app: "dark",
})
}}
>
Dark
</button>
<button
tabIndex={0}
className={clsx(
"py-1 my-2 ml-2 w-1/3 text-sm cursor-pointer select-none focus:outline-none",
{
active: app === "system",
"font-bold text-white": app === "system",
"text--gray-600": app !== "system",
},
)}
onKeyUp={(event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Tab")
updateTheme({
app: "system",
})
}}
onClick={() => {
updateTheme({
app: "system",
})
}}
>
System
</button>
</div>
</div>
</>
)
})
export default observer(function App() {
return <AppTheme />
})
Codesandbox → https://codesandbox.io/s/mobx-theme-change-animated-18gc6?file=/src/App.tsx
Idk why it isn't animating on Codesandbox but it works locally. Maybe a Codesandbox bug :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With