DEV Community

Cover image for From Idea to macOS-Style Portfolio: React, GSAP Animations & Higher-Order Components
Kunal Chakraborty
Kunal Chakraborty

Posted on

From Idea to macOS-Style Portfolio: React, GSAP Animations & Higher-Order Components

For this project, I set out to build a macOS-inspired portfolio using React, Tailwind CSS, and GSAP. At first, the goal was simple: create something visually impressive. But as the project evolved, it became much more than that. It turned into a deep exploration of animation systems, reusable architecture, and professional development workflows.

The final result isn’t just a portfolio—it behaves like an application, complete with draggable windows, smooth transitions, and a responsive Dock that mimics macOS interactions.

⚡ The Stack That Made It Possible

React acted as the backbone of the application. Instead of treating the UI as static pages, I approached it like an operating system. Each “app” is essentially a window with its own lifecycle—open, close, minimize, and focus. Managing which window stays on top required careful state handling, especially when dealing with z-index and user interactions.

For styling, Tailwind CSS made it surprisingly easy to recreate macOS’s signature glassmorphism. Using utilities like backdrop blur and semi-transparent backgrounds, I was able to achieve the frosted-glass effect without writing heavy custom CSS. This allowed me to focus more on behavior rather than styling complexity.

GSAP is where everything came to life. While CSS animations can handle basic transitions, they fall short when precision and performance are critical. GSAP provided the control needed to build fluid, responsive animations—especially for interactions like window transitions and Dock behavior.

🖱️ Recreating the macOS Dock Experience

One of the most satisfying parts of this project was implementing the Dock magnification effect. It’s a subtle detail, but it adds a lot to the overall experience.

The logic is based on proximity. As the cursor moves closer to an icon, the icon scales up smoothly. This required calculating the distance between the cursor and each icon, then mapping that distance to a scale value. GSAP made it easy to interpolate these values and animate them efficiently.

What seems like a small UI detail actually required careful tuning to feel natural. Too fast, and it feels jittery. Too slow, and it feels unresponsive. Getting this right made a huge difference in the final polish.

🏗️ Scaling the Window System with a Higher-Order Component

As the project grew, managing each window individually started to feel repetitive. Every window needed the same core behavior—animations, drag functionality, focus handling, and layering.

Rather than duplicating this logic across multiple components, I introduced a Higher-Order Component (HOC) called WindowWrapper.

This abstraction allowed me to treat every app window as a plug-and-play component while centralizing all the complex behavior in one place.

💡 The Idea Behind WindowWrapper

The goal was simple:

Wrap any component and instantly give it “macOS window behavior.”

This includes:

Open/close animations

Dragging with GSAP Draggable

Focus management (z-index handling)

Conditional rendering based on state

🧠 The Implementation

Here’s a simplified version of how it works:

`const WindowWrapper = (Component, windowKey) => {
    const Wrapped = (props) => {
        const { focusWindow, windows } = useWindowStore();
        const { isOpen, zIndex } = windows[windowKey];
        const ref = useRef(null);

        // Animation when opening
        useGSAP(() => {
            const el = ref.current;
            if (!el || !isOpen) return;

            el.style.display = "block";

            gsap.fromTo(
                el,
                { scale: 0.8, opacity: 0, y: 40 },
                { scale: 1, opacity: 1, y: 0, duration: 0.1, ease: "power4.out" }
            );
        }, [isOpen]);

        // Draggable behavior
        useGSAP(() => {
            const el = ref.current;
            if (!el) return;

            const header = el.querySelector('#window-header');

            const [instance] = Draggable.create(el, {
                trigger: header || el,
                onPress: () => focusWindow(windowKey)
            });

            return () => instance.kill();
        }, []);

        // Show/hide window
        useLayoutEffect(() => {
            const el = ref.current;
            if (!el) return;
            el.style.display = isOpen ? "block" : "none";
        }, [isOpen]);

        return (
            <section
                id={windowKey}
                ref={ref}
                style={{ zIndex }}
                className="absolute"
            >
                <Component {...props} />
            </section>
        );
    };

    return Wrapped;
};`
Enter fullscreen mode Exit fullscreen mode

⚙️ Why This Approach Works

This pattern solved multiple problems at once.

Instead of tightly coupling UI logic with individual components, I separated behavior from presentation. Each app component now focuses only on its UI, while the wrapper handles system-level responsibilities.

It also made the project incredibly scalable. Adding a new “app” is as simple as wrapping it:

const NotesAppWindow = WindowWrapper(NotesApp, "notes");

No extra logic. No duplication. Just plug and play.

⚙️ Thinking Beyond Code: Professional Workflow

At some point, I realized that writing good UI code wasn’t enough. Managing the project properly was just as important.

I moved away from pushing changes directly to the main branch and started using a feature-branch workflow. Every new feature—whether it was a clock in the menu bar or a new draggable window—was developed in its own branch and merged through a pull request. This gave me a chance to review my own code, clean things up, and avoid breaking existing features.

Versioning also became important as the project evolved. I followed semantic versioning to keep track of changes in a structured way. Major updates marked significant milestones, minor versions introduced new features, and patches handled bug fixes. This added clarity and made the project feel more like a real product.

To streamline deployment, I set up a CI/CD pipeline using GitHub Actions. Now, whenever a pull request is merged into the main branch, the project automatically builds and deploys to Netlify. If something breaks, the deployment fails, preventing unstable code from going live. This automation removed a lot of manual effort and made the workflow much more reliable."
Live_link:https://kunal-dev-portfolio-six.vercel.app
github_link:https://github.com/KunalChakraborty445

Top comments (0)