Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make animated tabs in Tailwind CSS?

I want to make an animated tab like:

animated tabs tailwind

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:

my tailwind animated tabs attempt

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?

like image 313
deadcoder0904 Avatar asked Jun 30 '26 00:06

deadcoder0904


2 Answers

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.

like image 61
Teuta Koraqi Avatar answered Jul 01 '26 14:07

Teuta Koraqi


Turns out, it is possible with pure Tailwind.

tailwind.config.js

module.exports = {
    theme: {
        extend: {
            translate: {
                200: '200%',
            },
        },
    },
}

App.tsx

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 :)

like image 24
deadcoder0904 Avatar answered Jul 01 '26 15:07

deadcoder0904