Skip to content

Remove @headlessui/react Transition in Favor of Pure CSS #129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

thewebartisan7
Copy link

@thewebartisan7 thewebartisan7 commented Jun 12, 2025

Summary

This PR removes the dependency on @headlessui/react for transition effects and replaces its usage with a simpler, Tailwind-based CSS approach. This helps streamline the stack and aligns the project more closely with the existing usage of shadcn/ui and Tailwind utility classes.

Changes

Replaced the Transition component from @headlessui/react in the Profile and Password page component success message with a Tailwind CSS opacity transition.

Updated conditional rendering logic to animate the "Saved" message using native CSS transitions (transition-opacity, opacity-0, opacity-100).

Removed unnecessary dependency on @headlessui/react where it was only used for this purpose.

Motivation

Maintain consistency with shadcn/ui, which is already used throughout the project.
Reduce bundle size by eliminating unused dependencies.
Simplify component logic by leveraging Tailwind’s built-in transition utilities.
Avoid importing a full animation library for minor UI feedback.

Impact

No functional change in user experience: the "Saved" message still fades in/out on successful form update.
Cleaner and more maintainable codebase.

@headlessui/react may now be removed from the project entirely.

Testing

Verified that the "Saved" message correctly appears and fades in/out when profile data is saved.
No visual regressions or console warnings observed during manual testing.

Before & After

Before:

<Transition
  show={recentlySuccessful}
  enter="transition ease-in-out"
  enterFrom="opacity-0"
  leave="transition ease-in-out"
  leaveTo="opacity-0"
>
  <p className="text-sm text-neutral-600">Saved</p>
</Transition>

After:

<p
  className={`text-sm text-neutral-600 transition-opacity duration-300 ${
    recentlySuccessful ? 'opacity-100' : 'opacity-0'
  }`}
>
  Saved
</p>

Remove headlessui/react dependency
Remove headlessui/react dependency
@rrmesquita
Copy link
Contributor

I might be wrong, but the reason for using the Transition component is to animate the conditional presence of the p tag. Simply setting the opacity would only make it invisible while still leaving the element in the layout, occupying space.

@thewebartisan7
Copy link
Author

thewebartisan7 commented Jun 23, 2025

I might be wrong, but the reason for using the Transition component is to animate the conditional presence of the p tag. Simply setting the opacity would only make it invisible while still leaving the element in the layout, occupying space.

Thanks for the feedback! You’re correct that toggling opacity keeps the <p> tag in the DOM, occupying ~40px x 20px of space when opacity-0. Given the small size and the flex items-center gap-4 layout, combined with Inertia’s 2-second recentlySuccessful duration, this shouldn’t impact the UI noticeably. If you do see any layout issues, you can switch to conditional rendering with a fade-out animation using for example:

const [showMessage, setShowMessage] = useState(false);
useEffect(() => {
    if (recentlySuccessful) {
        setShowMessage(true);
    } else {
        const timer = setTimeout(() => setShowMessage(false), 500);
        return () => clearTimeout(timer);
    }
}, [recentlySuccessful]);

{showMessage && (
    <p
        className={`text-sm text-neutral-600 transition-opacity duration-500 ${
            recentlySuccessful ? 'opacity-100' : 'opacity-0'
        }`}
    >
        Saved
    </p>
)}

To make this logic reusable across components, you can extract it into a custom React hook, e.g.:

const useFadeTransition = (trigger, duration = 500) => {
    const [show, setShow] = useState(false);

    useEffect(() => {
        if (trigger) {
            setShow(true);
        } else {
            const timer = setTimeout(() => setShow(false), duration);
            return () => clearTimeout(timer);
        }
    }, [trigger, duration]);

    return show;
};

// Usage:
const showMessage = useFadeTransition(recentlySuccessful, 500);

{showMessage && (
    <p
        className={`text-sm text-neutral-600 transition-opacity duration-500 ${
            recentlySuccessful ? 'opacity-100' : 'opacity-0'
        }`}
    >
        Saved
    </p>
)}

or even as custom component:

import { useState, useEffect } from 'react';

const FadeTransition = ({ showWhen, children, duration = 500 }) => {
    const [show, setShow] = useState(false);

    useEffect(() => {
        if (showWhen) {
            setShow(true);
        } else {
            const timer = setTimeout(() => setShow(false), duration);
            return () => clearTimeout(timer);
        }
    }, [showWhen, duration]);

    return show ? (
        <div
            className={`transition-opacity duration-${duration} ${
                showWhen ? 'opacity-100' : 'opacity-0'
            }`}
        >
            {children}
        </div>
    ) : null;
};

// Usage in component:
<div className="flex items-center gap-4">
    <Button disabled={processing}>Save</Button>
    <FadeTransition showWhen={recentlySuccessful}>
        <p className="text-sm text-neutral-600" aria-live="polite">
            Saved
        </p>
    </FadeTransition>
</div>

In my opinion, simply setting opacity for the small <p> tag is not a problem in most cases within the current layout, as the minimal space it occupies (~40px x 20px) doesn’t disrupt the flex container’s design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants