Light Leak
Light Leak

Liquid Button

An animated button component with a liquid effect and various styles.

Introduction

The ZyfloLiquidButton component is a customizable and animated button that creates a liquid effect when hovered. It offers various styles, sizes, and optional animations, making it a visually appealing interactive element for your Next.js project.

Add The Component

Add the following component to your project in the /components/zyflo directory:

"use client"

import React, { useRef } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import {
  cn,
  getAutoContrastClassName,
  getCSSVariable,
  areColorsCompatible
} from "@/lib/utils"
import { motion, Variants } from "framer-motion"

export type ZyfloLiquidButtonVariant =
  | "success"
  | "info"
  | "warning"
  | "danger"
  | "default"
  | "secondary"
  | "gradient"
export type ZyfloLiquidButtonSize = "default" | "sm" | "lg"

export const PossibleZyfloLiquidButtonVariant = [
  "success",
  "info",
  "warning",
  "danger",
  "default",
  "secondary",
  "gradient"
] as const
export const PossibleZyfloLiquidButtonSize = ["default", "sm", "lg"] as const

const liquidButtonVariants = cva(
  "relative overflow-hidden rounded-lg font-bold no-underline zyflo-transition",
  {
    variants: {
      variant: {
        success:
          "bg-emerald-500 text-white hover:[box-shadow:0_0_25px_theme('colors.emerald.500/50%'),inset_0_0_25px_theme('colors.emerald.500/50%')]",
        info: "bg-blue-500 text-white hover:[box-shadow:0_0_25px_theme('colors.blue.500/50%'),inset_0_0_25px_theme('colors.blue.500/50%')]",
        warning:
          "bg-yellow-500 text-white hover:[box-shadow:0_0_25px_theme('colors.yellow.500/50%'),inset_0_0_25px_theme('colors.yellow.500/50%')]",
        danger:
          "bg-red-500 text-white hover:[box-shadow:0_0_25px_theme('colors.red.500/50%'),inset_0_0_25px_theme('colors.red.500/50%')]",
        default:
          "border-primary/30 text-white bg-primary hover:border-primary/80 dark:border-primary/40 dark:bg-primary dark:hover:border-primary/70 hover:[box-shadow:0_0_25px_theme('colors.primary.DEFAULT/50%'),inset_0_0_25px_theme('colors.primary.DEFAULT/50%')]",
        secondary:
          "bg-secondary text-gray-950 dark:text-gray-50 hover:[box-shadow:0_0_25px_hsl(var(--foreground)/0.08)]",
        gradient:
          "bg-gradient-to-r bg-[size:300%_300%] hover:bg-[position:0%_0%] bg-[position:100%_100%] from-primary to-primary via-accent border-primary/10 border-2 hover:border-primary/30 dark:[box-shadow:0_3px_30px_1px_hsl(var(--primary)/0.4)] dark:hover:[box-shadow:0_4px_40px_1px_hsl(var(--primary)/0.5)] hover:[box-shadow:0_4px_40px_1px_hsl(var(--primary)/0.4)] [box-shadow:0_3px_30px_1px_hsl(var(--primary)/0.3)]"
      },
      size: {
        default: "px-6 py-3 text-base",
        sm: "px-4 py-2 text-sm",
        lg: "px-8 py-4 text-lg"
      }
    },
    defaultVariants: { variant: "default", size: "default" }
  }
)

const liquidVariants = cva(
  "absolute left-0 top-[-60px] h-[200px] w-full zyflo-transition group-hover:-top-[220px]",
  {
    variants: {
      variant: {
        success: [
          "bg-emerald-500",
          "before:bg-emerald-100",
          "after:bg-emerald-500"
        ],
        info: ["bg-blue-500", "before:bg-blue-100", "after:bg-blue-500"],
        warning: [
          "bg-yellow-500",
          "before:bg-yellow-100",
          "after:bg-yellow-500"
        ],
        danger: ["bg-red-500", "before:bg-red-100", "after:bg-red-500"],
        default: ["bg-primary", "before:bg-primary-100", "after:bg-primary"],
        secondary: [
          "bg-secondary",
          "before:bg-foreground/10",
          "after:bg-foreground/10"
        ],
        gradient: [
          "bg-gradient-to-r from-primary to-primary via-accent",
          "dark:before:bg-background before:bg-background",
          "dark:after:bg-background after:bg-background"
        ]
      }
    },
    defaultVariants: { variant: "default" }
  }
)

export interface ZyfloLiquidButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof liquidButtonVariants> {
  href?: string
  label?: string
  srOnly?: string
  disableAnimations?: boolean
  triggerWhenInView?: boolean
}

const ZyfloLiquidButton = React.forwardRef<
  HTMLButtonElement | HTMLAnchorElement,
  ZyfloLiquidButtonProps & React.AnchorHTMLAttributes<HTMLAnchorElement>
>(
  (
    {
      className,
      variant,
      size,
      href,
      children,
      label,
      srOnly,
      disableAnimations = false,
      triggerWhenInView = true,
      ...props
    },
    ref
  ) => {
    const Comp = href ? "a" : "button"
    const MotionComp = disableAnimations
      ? Comp
      : (motion[Comp as keyof typeof motion] as any)
    const textRef = useRef<HTMLSpanElement>(null)
    const primaryHSL = getCSSVariable("--primary")
      .replace("%", "")
      .slice(0, -1)
      .split(" ")
      .map(Number)

    const primaryAndBlackAreCompatible = areColorsCompatible(
      primaryHSL[0],
      primaryHSL[1],
      primaryHSL[2],
      0,
      0,
      0
    )
    const primaryAndWhiteAreCompatible = areColorsCompatible(
      primaryHSL[0],
      primaryHSL[1],
      primaryHSL[2],
      100,
      100,
      100
    )

    const gradientStyle =
      variant === "gradient"
        ? {
            color: getAutoContrastClassName(
              primaryAndBlackAreCompatible,
              primaryAndWhiteAreCompatible
            )
          }
        : {}

    const containerVariants: Variants = {
      hidden: { y: 50, opacity: 0 },
      visible: { y: 0, opacity: 1, transition: { duration: 0.3 } }
    }

    const textVariants: Variants = {
      hidden: { scale: 0 },
      visible: { scale: 1, transition: { delay: 0.3, duration: 0.3 } }
    }

    const animationProps = disableAnimations
      ? {}
      : {
          variants: containerVariants,
          initial: "hidden",
          ...(triggerWhenInView
            ? { whileInView: "visible", viewport: { once: true } }
            : { animate: "visible" })
        }

    return (
      <MotionComp
        className={cn(
          "group",
          liquidButtonVariants({ variant, size, className })
        )}
        ref={ref}
        href={href}
        aria-label={label}
        {...animationProps}
        {...props}
      >
        <motion.span
          className={cn(`relative z-10`)}
          style={{
            backgroundColor:
              variant === "gradient" ? "rgba(0,0,0,0)" : "transparent",
            ...gradientStyle
          }}
          ref={textRef}
          variants={textVariants}
        >
          {children}
        </motion.span>
        {srOnly && <span className="sr-only">{srOnly}</span>}
        {!disableAnimations && (
          <div
            className={cn(
              liquidVariants({ variant }),
              size === "sm" && "-top-[20px]",
              size === "default" && "-top-[42px]",
              size === "lg" && "-top-[48px]",
              "before:absolute before:left-0 before:top-2 before:h-[200%] before:w-[200%] before:-translate-x-1/4 before:-translate-y-3/4 before:animate-liquid-slow before:rounded-[40%] before:opacity-40",
              "after:absolute after:left-0 after:top-0 after:h-[200%] after:w-[200%] after:-translate-x-1/3 after:-translate-y-2/4 after:animate-liquid-fast after:rounded-[50%] after:opacity-60"
            )}
          />
        )}
      </MotionComp>
    )
  }
)

ZyfloLiquidButton.displayName = "ZyfloLiquidButton"

export default ZyfloLiquidButton

Usage

Here's a basic example of how to use the ZyfloLiquidButton component:

import { ZyfloLiquidButton } from "@/components/zyflo/liquid-button"
export default function MyPage() {
    return (
        <ZyfloLiquidButton variant="default" size="default">
            Click me
        </ZyfloLiquidButton>
    )
}

Examples

Here's a comprehensive example showcasing all variants and sizes of the ZyfloLiquidButton component:

Zyflo Window Preview
success
info
warning
danger
default
secondary
gradient

Props

Quick Props Overview

ZyfloLiquidButton Component Props
PropDescription
variantDefines the visual style of the button
sizeDefines the size of the button
hrefIf provided, renders the button as an anchor tag
srOnlyText for screen readers only
labelAccessible label for the button
triggerWhenInViewIf true, the animation will be triggered when the button is in the viewport
disableAnimationsIf true, disables all animations
classNameAdditional CSS classes

Detailed Props Overview

variant

  • Type: ZyfloLiquidButtonVariant
  • Default: "default"
  • Possible values: "success", "info", "warning", "danger", "default", "secondary", "gradient"

Defines the visual style of the button.

size

  • Type: ZyfloLiquidButtonSize
  • Default: "default"
  • Possible values: "default", "sm", "lg"

Defines the size of the button.

href

  • Type: string
  • Optional

If provided, renders the button as an anchor tag with the specified URL.

srOnly

  • Type: string
  • Optional

Adds a span with the sr-only class containing the provided text. This text will be invisible on screen but readable by screen readers.

label

  • Type: string
  • Optional

Adds an aria-label attribute to the button element. This provides an accessible name for the button, which can be useful for screen readers.

triggerWhenInView

  • Type: boolean
  • Default: false

If set to true, the animation will be triggered when the button is in the viewport.

disableAnimations

  • Type: boolean
  • Default: false

If set to true, disables all animations in the button.

className

  • Type: string
  • Optional

Additional CSS classes to be applied to the button.

Customization

The component uses Tailwind CSS classes for styling. You can customize its appearance by modifying the CSS classes in the component's source code or by passing additional classes through the className prop.

Accessibility

The component is designed with accessibility in mind:

  • It uses semantic HTML elements (button or anchor) for proper structure.
  • The component supports keyboard focus and includes appropriate focus styles.
  • The srOnly prop allows you to provide additional context for screen readers without affecting the visual appearance.
  • The label prop provides a way to add an accessible name to the button, enhancing its description for screen readers.

Notes

  • The component uses Framer Motion for animations. Make sure you have Framer Motion installed in your project if you plan to use animations.
  • The gradient variant automatically adjusts its text color based on the primary color to ensure proper contrast.

Contributing

If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on our GitHub repository. We appreciate your contributions and are always open to collaboration.

Thank you for considering contributing to Zyflo!