components

Blocks

Interactive UI

Fill Buttons

Source

The Fill Buttons component replaces the standard buttons from shadcn to interactive dual-tone color buttons.

Note: Replace the code with buttons component after installing shadcn button.
Important: Wrap the text with a span

fillDefault

fillSecondary

fillGhost

fillOutline

CODE
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";

import { cn } from "@/lib/utils";

const buttonVariants = cva(
    "group/button relative isolate inline-flex shrink-0 items-center justify-center overflow-hidden border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-[color,border-color,box-shadow,transform,background-color] outline-none select-none before:pointer-events-none before:absolute before:inset-0 before:z-0 before:origin-left before:scale-x-0 before:bg-[var(--button-fill)] before:transition-transform before:duration-300 before:ease-out hover:before:scale-x-100 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&>*]:relative [&>*]:z-10 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
    {
        variants: {
            variant: {
                default:
                    "bg-primary text-primary-foreground hover:bg-primary/80",
                outline:
                    "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
                secondary:
                    "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
                ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
                destructive:
                    "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
                link: "text-primary underline-offset-4 hover:underline",
                // fill buttons
                fillDefault: [
                    "bg-primary text-primary-foreground",
                    "[--button-fill:var(--primary-foreground)]",
                    "hover:text-primary",
                    "dark:hover:text-primary",
                ].join(" "),
                fillOutline: [
                    "border-border bg-background text-foreground",
                    "[--button-fill:var(--foreground)]",
                    "hover:text-background",
                    "dark:hover:text-background",
                    "aria-expanded:bg-muted aria-expanded:text-foreground",
                    "dark:border-input dark:bg-input/30",
                ].join(" "),
                fillSecondary: [
                    "bg-secondary text-secondary-foreground",
                    "[--button-fill:var(--secondary-foreground)]",
                    "hover:text-secondary",
                    "dark:hover:text-secondary",
                    "aria-expanded:bg-secondary",
                ].join(" "),
                fillGhost: [
                    "bg-transparent text-foreground",
                    "[--button-fill:var(--foreground)]",
                    "hover:text-background",
                    "dark:hover:text-background",
                    "aria-expanded:bg-muted aria-expanded:text-foreground",
                ].join(" "),
            },
            size: {
                default:
                    "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
                xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
                sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
                lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
                icon: "size-9",
                "icon-xs":
                    "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
                "icon-sm":
                    "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
                "icon-lg": "size-10",
            },
        },
        defaultVariants: {
            variant: "default",
            size: "default",
        },
    },
);

function Button({
    className,
    variant = "default",
    size = "default",
    asChild = false,
    ...props
}: React.ComponentProps<"button"> &
    VariantProps<typeof buttonVariants> & {
        asChild?: boolean;
    }) {
    const Comp = asChild ? Slot.Root : "button";

    return (
        <Comp
            data-slot="button"
            data-variant={variant}
            data-size={size}
            className={cn(buttonVariants({ variant, size, className }))}
            {...props}
        />
    );
}

export { Button, buttonVariants };

Interactive UI

Rect Tip

Source

The Cursor component replaces the standard browser pointer with a minimalist, interactive dual-element system. It is context-aware, automatically adjusting its shape and behavior based on the elements it interacts with.

The RectTip is actually a hyperlink

Dependency Checklist: motion/react

Props: heading, description, photo, link, width, height

Note: The RectTip is actually a hyperlink
CODE

Theme Management

Theme Clipper

Source

A smooth theme switcher utilizing the View Transition API for a circular expansion effect. It persists user preferences and respects system settings.

Dependency Checklist: motion/react | react-hot-toast

SVG Assets: Requires Moon.svg and Sun.svg to be present in your public folder.

View Transition Styles

::view-transition-old(root),
::view-transition-new(root) {
    animation: none;
    mix-blend-mode: normal;
}

* {
    transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
        border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
        color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
        box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

Live Preview

COMPONENT

Interactive UI

Blob Cursor

Source

The Cursor component replaces the standard browser pointer with a minimalist, interactive dual-element system. It is context-aware, automatically adjusting its shape and behavior based on the elements it interacts with.

Note: Wrap this component in CursorProvider before adding to layout.tsx and use data-cursor-hover to enable interactions.
CODE
"use client";

import { useEffect, useRef, useState } from "react";
import gsap from "gsap";

const Cursor = () => {
    const cursorRef = useRef(null);
    const backdropRef = useRef(null);
    const [isMobile, setIsMobile] = useState(false);

    useEffect(() => {
        const checkMobile = () => {
            setIsMobile(
                /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
                window.innerWidth <= 768 ||
                "ontouchstart" in window
            );
        };
        checkMobile();
        window.addEventListener("resize", checkMobile);
        return () => window.removeEventListener("resize", checkMobile);
    }, []);

    useEffect(() => {
        if (isMobile || !cursorRef.current || !backdropRef.current) return;

        const cursor = cursorRef.current;
        const backdrop = backdropRef.current;
        
        let mouseX = 0;
        let mouseY = 0;
        let isHovering = false;

        gsap.set([cursor, backdrop], { xPercent: -50, yPercent: -50, opacity: 1 });

        const updatePosition = (e) => {
            mouseX = e.clientX;
            mouseY = e.clientY;
            
            gsap.set(cursor, { x: mouseX, y: mouseY });

            if (!isHovering) {
                gsap.to(backdrop, {
                    x: mouseX,
                    y: mouseY,
                    duration: 0.15,
                    ease: "power3.out",
                });
            }
        };

        const handleHover = (e) => {
            isHovering = true;
            const target = e.currentTarget;
            const rect = target.getBoundingClientRect();

            gsap.killTweensOf(backdrop);

            gsap.to(backdrop, {
                x: rect.left + rect.width / 2,
                y: rect.top + rect.height / 2,
                width: rect.width + 10,
                height: rect.height + 4,
                borderRadius: "8px",
                backgroundColor: "rgba(255, 255, 255, 0.1)",
                duration: 0.3,
                ease: "power3.out",
            });

            gsap.to(cursor, { opacity: 0, duration: 0.3 });
        };

        const handleUnHover = () => {
            isHovering = false;
            gsap.to(backdrop, {
                width: 16,
                height: 16,
                borderRadius: "100%",
                backgroundColor: "rgba(0, 0, 0, 0.1)",
                duration: 0.5,
                ease: "power3.out",
            });
            gsap.to(cursor, { opacity: 1, duration: 0.3 });
        };

        window.addEventListener("mousemove", updatePosition);

        const refreshListeners = () => {
            const elements = document.querySelectorAll("a, button, [data-cursor-hover]");
            elements.forEach((el) => {
                el.addEventListener("mouseenter", handleHover);
                el.addEventListener("mouseleave", handleUnHover);
            });
            return elements;
        };

        const hoverElements = refreshListeners();

        return () => {
            window.removeEventListener("mousemove", updatePosition);
            hoverElements.forEach((el) => {
                el.removeEventListener("mouseenter", handleHover);
                el.removeEventListener("mouseleave", handleUnHover);
            });
        };
    }, [isMobile]);

    if (isMobile) return null;

    return (
        <>
            <div ref={cursorRef} className="fixed top-0 left-0 pointer-events-none w-2 h-2 rounded-full bg-white mix-blend-difference z-9998" />
            <div ref={backdropRef} className="fixed top-0 left-0 pointer-events-none w-4 h-4 rounded-full border border-slate-400 bg-white/5 z-9997" style={{willChange: "width, height, transform"}} />
        </>
    );
};

export default Cursor;