UI Components Easy
Interactive Hover Button
A button with creative multi-layered hover effects including background fill, text color inversion, and border animation.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f1f5f9;
min-height: 100vh;
}
.demo {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 2rem;
}
.demo-title {
font-size: 1.5rem;
font-weight: 700;
color: #e2e8f0;
}
.hint {
color: #525252;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.button-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 2rem;
}
/* --- Interactive hover button base --- */
.ihover-btn {
--fill-color: #f1f5f9;
--text-color: #e2e8f0;
--text-hover: #0a0a0a;
--border-color: #334155;
--border-hover: #f1f5f9;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.875rem 2rem;
border-radius: 10px;
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: 0.01em;
cursor: pointer;
background: transparent;
border: 1.5px solid var(--border-color);
outline: none;
overflow: hidden;
transition: border-color 0.35s ease;
}
.ihover-btn:hover {
border-color: var(--border-hover);
}
/* Fill from bottom */
.ihover-btn::before {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 0;
background: var(--fill-color);
transition: height 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
z-index: 0;
}
.ihover-btn:hover::before {
height: 100%;
}
/* Label */
.ihover-btn__label {
position: relative;
z-index: 1;
color: var(--text-color);
transition: color 0.35s ease;
}
.ihover-btn:hover .ihover-btn__label {
color: var(--text-hover);
}
/* Sizes */
.ihover-btn--sm {
padding: 0.625rem 1.5rem;
font-size: 0.8125rem;
}
.ihover-btn--lg {
padding: 1.125rem 2.75rem;
font-size: 1.0625rem;
}
/* Variant: Default (white fill) */
.ihover-btn--default {
--fill-color: #f1f5f9;
--text-color: #e2e8f0;
--text-hover: #0a0a0a;
--border-color: #475569;
--border-hover: #f1f5f9;
}
/* Variant: Blue */
.ihover-btn--blue {
--fill-color: #3b82f6;
--text-color: #93c5fd;
--text-hover: #fff;
--border-color: #3b82f6;
--border-hover: #60a5fa;
}
/* Variant: Purple */
.ihover-btn--purple {
--fill-color: #8b5cf6;
--text-color: #c4b5fd;
--text-hover: #fff;
--border-color: #8b5cf6;
--border-hover: #a78bfa;
}
/* Variant: Emerald */
.ihover-btn--emerald {
--fill-color: #10b981;
--text-color: #6ee7b7;
--text-hover: #fff;
--border-color: #10b981;
--border-hover: #34d399;
}
/* Variant: Rose */
.ihover-btn--rose {
--fill-color: #f43f5e;
--text-color: #fda4af;
--text-hover: #fff;
--border-color: #f43f5e;
--border-hover: #fb7185;
}
/* Variant: Slide right */
.ihover-btn--slide-right::before {
bottom: auto;
top: 0;
left: 0;
width: 0;
height: 100%;
transition: width 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.ihover-btn--slide-right:hover::before {
width: 100%;
}
.ihover-btn--slide-right {
--fill-color: #f1f5f9;
--text-color: #e2e8f0;
--text-hover: #0a0a0a;
--border-color: #475569;
--border-hover: #f1f5f9;
}
/* Variant: Curtain (from center) */
.ihover-btn--curtain::before {
bottom: auto;
top: 50%;
left: 0;
width: 100%;
height: 0;
transform: translateY(-50%);
transition: height 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.ihover-btn--curtain:hover::before {
height: 100%;
}
.ihover-btn--curtain {
--fill-color: #8b5cf6;
--text-color: #c4b5fd;
--text-hover: #fff;
--border-color: #8b5cf6;
--border-hover: #a78bfa;
}
/* Variant: Glow */
.ihover-btn--glow {
--fill-color: #3b82f6;
--text-color: #93c5fd;
--text-hover: #fff;
--border-color: #3b82f6;
--border-hover: #60a5fa;
transition: border-color 0.35s ease, box-shadow 0.35s ease;
}
.ihover-btn--glow:hover {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2);
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.ihover-btn::before {
transition: none;
}
.ihover-btn__label {
transition: none;
}
.ihover-btn {
transition: none;
}
}// Interactive Hover Button — minimal JS (CSS handles all hover effects)
(function () {
"use strict";
// No JS needed — all hover transitions are handled by CSS
// This file is kept for consistency with the snippet structure
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Interactive Hover Button</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h2 class="demo-title">Interactive Hover Buttons</h2>
<p class="hint">Hover over the buttons to see the fill effect</p>
<div class="button-row">
<button class="ihover-btn ihover-btn--default">
<span class="ihover-btn__label">Explore</span>
</button>
<button class="ihover-btn ihover-btn--blue">
<span class="ihover-btn__label">Get Started</span>
</button>
<button class="ihover-btn ihover-btn--purple">
<span class="ihover-btn__label">Learn More</span>
</button>
</div>
<div class="button-row">
<button class="ihover-btn ihover-btn--emerald ihover-btn--lg">
<span class="ihover-btn__label">Download Now</span>
</button>
<button class="ihover-btn ihover-btn--rose ihover-btn--sm">
<span class="ihover-btn__label">Sign Up</span>
</button>
</div>
<div class="button-row">
<button class="ihover-btn ihover-btn--slide-right">
<span class="ihover-btn__label">Slide Right</span>
</button>
<button class="ihover-btn ihover-btn--curtain">
<span class="ihover-btn__label">Curtain Reveal</span>
</button>
<button class="ihover-btn ihover-btn--glow">
<span class="ihover-btn__label">Glow Effect</span>
</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, type CSSProperties, type ReactNode, type MouseEvent } from "react";
type Variant =
| "default"
| "blue"
| "purple"
| "emerald"
| "rose"
| "slide-right"
| "curtain"
| "glow";
interface InteractiveHoverButtonProps {
children: ReactNode;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
variant?: Variant;
size?: "sm" | "md" | "lg";
className?: string;
}
const variantConfig: Record<
Variant,
{ fill: string; text: string; textHover: string; border: string; borderHover: string }
> = {
default: {
fill: "#f1f5f9",
text: "#e2e8f0",
textHover: "#0a0a0a",
border: "#475569",
borderHover: "#f1f5f9",
},
blue: {
fill: "#3b82f6",
text: "#93c5fd",
textHover: "#fff",
border: "#3b82f6",
borderHover: "#60a5fa",
},
purple: {
fill: "#8b5cf6",
text: "#c4b5fd",
textHover: "#fff",
border: "#8b5cf6",
borderHover: "#a78bfa",
},
emerald: {
fill: "#10b981",
text: "#6ee7b7",
textHover: "#fff",
border: "#10b981",
borderHover: "#34d399",
},
rose: {
fill: "#f43f5e",
text: "#fda4af",
textHover: "#fff",
border: "#f43f5e",
borderHover: "#fb7185",
},
"slide-right": {
fill: "#f1f5f9",
text: "#e2e8f0",
textHover: "#0a0a0a",
border: "#475569",
borderHover: "#f1f5f9",
},
curtain: {
fill: "#8b5cf6",
text: "#c4b5fd",
textHover: "#fff",
border: "#8b5cf6",
borderHover: "#a78bfa",
},
glow: {
fill: "#3b82f6",
text: "#93c5fd",
textHover: "#fff",
border: "#3b82f6",
borderHover: "#60a5fa",
},
};
const sizeStyles: Record<string, CSSProperties> = {
sm: { padding: "0.625rem 1.5rem", fontSize: "0.8125rem" },
md: { padding: "0.875rem 2rem", fontSize: "0.9375rem" },
lg: { padding: "1.125rem 2.75rem", fontSize: "1.0625rem" },
};
export function InteractiveHoverButton({
children,
onClick,
variant = "default",
size = "md",
className = "",
}: InteractiveHoverButtonProps) {
const [hovered, setHovered] = useState(false);
const config = variantConfig[variant];
const isSlideRight = variant === "slide-right";
const isCurtain = variant === "curtain";
const isGlow = variant === "glow";
const btnStyle: CSSProperties = {
position: "relative",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "10px",
fontWeight: 600,
letterSpacing: "0.01em",
cursor: "pointer",
background: "transparent",
border: `1.5px solid ${hovered ? config.borderHover : config.border}`,
outline: "none",
overflow: "hidden",
transition: "border-color 0.35s ease, box-shadow 0.35s ease",
boxShadow:
isGlow && hovered ? "0 0 20px rgba(59,130,246,0.4), 0 0 40px rgba(59,130,246,0.2)" : "none",
...sizeStyles[size],
};
const fillStyle: CSSProperties = isSlideRight
? {
position: "absolute",
top: 0,
left: 0,
width: hovered ? "100%" : "0",
height: "100%",
background: config.fill,
transition: "width 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)",
zIndex: 0,
}
: isCurtain
? {
position: "absolute",
top: "50%",
left: 0,
width: "100%",
height: hovered ? "100%" : "0",
transform: "translateY(-50%)",
background: config.fill,
transition: "height 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)",
zIndex: 0,
}
: {
position: "absolute",
bottom: 0,
left: 0,
width: "100%",
height: hovered ? "100%" : "0",
background: config.fill,
transition: "height 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)",
zIndex: 0,
};
const labelStyle: CSSProperties = {
position: "relative",
zIndex: 1,
color: hovered ? config.textHover : config.text,
transition: "color 0.35s ease",
};
return (
<button
onClick={onClick}
className={className}
style={btnStyle}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<span style={fillStyle} />
<span style={labelStyle}>{children}</span>
</button>
);
}
export default function InteractiveHoverButtonDemo() {
return (
<div
style={{
minHeight: "100vh",
background: "#0a0a0a",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "2rem",
padding: "2rem",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
<h2 style={{ fontSize: "1.5rem", fontWeight: 700, color: "#e2e8f0" }}>
Interactive Hover Buttons
</h2>
<p style={{ color: "#525252", fontSize: "0.875rem" }}>
Hover over the buttons to see the fill effect
</p>
<div style={{ display: "flex", flexWrap: "wrap", gap: "2rem", justifyContent: "center" }}>
<InteractiveHoverButton variant="default">Explore</InteractiveHoverButton>
<InteractiveHoverButton variant="blue">Get Started</InteractiveHoverButton>
<InteractiveHoverButton variant="purple">Learn More</InteractiveHoverButton>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "2rem", justifyContent: "center" }}>
<InteractiveHoverButton variant="emerald" size="lg">
Download Now
</InteractiveHoverButton>
<InteractiveHoverButton variant="rose" size="sm">
Sign Up
</InteractiveHoverButton>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "2rem", justifyContent: "center" }}>
<InteractiveHoverButton variant="slide-right">Slide Right</InteractiveHoverButton>
<InteractiveHoverButton variant="curtain">Curtain Reveal</InteractiveHoverButton>
<InteractiveHoverButton variant="glow">Glow Effect</InteractiveHoverButton>
</div>
</div>
);
}<script setup>
import { reactive } from "vue";
const variantConfig = {
default: {
fill: "#f1f5f9",
text: "#e2e8f0",
textHover: "#0a0a0a",
border: "#475569",
borderHover: "#f1f5f9",
},
blue: {
fill: "#3b82f6",
text: "#93c5fd",
textHover: "#fff",
border: "#3b82f6",
borderHover: "#60a5fa",
},
purple: {
fill: "#8b5cf6",
text: "#c4b5fd",
textHover: "#fff",
border: "#8b5cf6",
borderHover: "#a78bfa",
},
emerald: {
fill: "#10b981",
text: "#6ee7b7",
textHover: "#fff",
border: "#10b981",
borderHover: "#34d399",
},
rose: {
fill: "#f43f5e",
text: "#fda4af",
textHover: "#fff",
border: "#f43f5e",
borderHover: "#fb7185",
},
"slide-right": {
fill: "#f1f5f9",
text: "#e2e8f0",
textHover: "#0a0a0a",
border: "#475569",
borderHover: "#f1f5f9",
},
curtain: {
fill: "#8b5cf6",
text: "#c4b5fd",
textHover: "#fff",
border: "#8b5cf6",
borderHover: "#a78bfa",
},
glow: {
fill: "#3b82f6",
text: "#93c5fd",
textHover: "#fff",
border: "#3b82f6",
borderHover: "#60a5fa",
},
};
const sizeStyles = {
sm: "padding: 0.625rem 1.5rem; font-size: 0.8125rem;",
md: "padding: 0.875rem 2rem; font-size: 0.9375rem;",
lg: "padding: 1.125rem 2.75rem; font-size: 1.0625rem;",
};
const buttons = [
{ id: "a", variant: "default", size: "md", label: "Explore" },
{ id: "b", variant: "blue", size: "md", label: "Get Started" },
{ id: "c", variant: "purple", size: "md", label: "Learn More" },
{ id: "d", variant: "emerald", size: "lg", label: "Download Now" },
{ id: "e", variant: "rose", size: "sm", label: "Sign Up" },
{ id: "f", variant: "slide-right", size: "md", label: "Slide Right" },
{ id: "g", variant: "curtain", size: "md", label: "Curtain Reveal" },
{ id: "h", variant: "glow", size: "md", label: "Glow Effect" },
];
const hoverStates = reactive({});
function btnStyle(variant, size, hovered) {
const config = variantConfig[variant];
const isGlow = variant === "glow";
const boxShadow =
isGlow && hovered ? "0 0 20px rgba(59,130,246,0.4), 0 0 40px rgba(59,130,246,0.2)" : "none";
return `position:relative;display:inline-flex;align-items:center;justify-content:center;border-radius:10px;font-weight:600;letter-spacing:0.01em;cursor:pointer;background:transparent;border:1.5px solid ${hovered ? config.borderHover : config.border};outline:none;overflow:hidden;transition:border-color 0.35s ease,box-shadow 0.35s ease;box-shadow:${boxShadow};${sizeStyles[size]}`;
}
function fillStyle(variant, hovered) {
const config = variantConfig[variant];
const isSlideRight = variant === "slide-right";
const isCurtain = variant === "curtain";
if (isSlideRight) {
return `position:absolute;top:0;left:0;width:${hovered ? "100%" : "0"};height:100%;background:${config.fill};transition:width 0.35s cubic-bezier(0.25,0.46,0.45,0.94);z-index:0;`;
}
if (isCurtain) {
return `position:absolute;top:50%;left:0;width:100%;height:${hovered ? "100%" : "0"};transform:translateY(-50%);background:${config.fill};transition:height 0.35s cubic-bezier(0.25,0.46,0.45,0.94);z-index:0;`;
}
return `position:absolute;bottom:0;left:0;width:100%;height:${hovered ? "100%" : "0"};background:${config.fill};transition:height 0.35s cubic-bezier(0.25,0.46,0.45,0.94);z-index:0;`;
}
function labelStyle(variant, hovered) {
const config = variantConfig[variant];
return `position:relative;z-index:1;color:${hovered ? config.textHover : config.text};transition:color 0.35s ease;`;
}
</script>
<template>
<div class="hover-btn-demo">
<h2 class="demo-title">Interactive Hover Buttons</h2>
<p class="demo-subtitle">Hover over the buttons to see the fill effect</p>
<div class="btn-row">
<button
v-for="btn in buttons.slice(0, 3)"
:key="btn.id"
:style="btnStyle(btn.variant, btn.size, hoverStates[btn.id])"
@mouseenter="hoverStates[btn.id] = true"
@mouseleave="hoverStates[btn.id] = false"
>
<span :style="fillStyle(btn.variant, hoverStates[btn.id])" />
<span :style="labelStyle(btn.variant, hoverStates[btn.id])">{{ btn.label }}</span>
</button>
</div>
<div class="btn-row">
<button
v-for="btn in buttons.slice(3, 5)"
:key="btn.id"
:style="btnStyle(btn.variant, btn.size, hoverStates[btn.id])"
@mouseenter="hoverStates[btn.id] = true"
@mouseleave="hoverStates[btn.id] = false"
>
<span :style="fillStyle(btn.variant, hoverStates[btn.id])" />
<span :style="labelStyle(btn.variant, hoverStates[btn.id])">{{ btn.label }}</span>
</button>
</div>
<div class="btn-row">
<button
v-for="btn in buttons.slice(5)"
:key="btn.id"
:style="btnStyle(btn.variant, btn.size, hoverStates[btn.id])"
@mouseenter="hoverStates[btn.id] = true"
@mouseleave="hoverStates[btn.id] = false"
>
<span :style="fillStyle(btn.variant, hoverStates[btn.id])" />
<span :style="labelStyle(btn.variant, hoverStates[btn.id])">{{ btn.label }}</span>
</button>
</div>
</div>
</template>
<style scoped>
.hover-btn-demo {
min-height: 100vh;
background: #0a0a0a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 2rem;
font-family: system-ui, -apple-system, sans-serif;
}
.demo-title {
font-size: 1.5rem;
font-weight: 700;
color: #e2e8f0;
margin: 0;
}
.demo-subtitle {
color: #525252;
font-size: 0.875rem;
margin: 0;
}
.btn-row {
display: flex;
flex-wrap: wrap;
gap: 2rem;
justify-content: center;
}
</style><script>
const variantConfig = {
default: {
fill: "#f1f5f9",
text: "#e2e8f0",
textHover: "#0a0a0a",
border: "#475569",
borderHover: "#f1f5f9",
},
blue: {
fill: "#3b82f6",
text: "#93c5fd",
textHover: "#fff",
border: "#3b82f6",
borderHover: "#60a5fa",
},
purple: {
fill: "#8b5cf6",
text: "#c4b5fd",
textHover: "#fff",
border: "#8b5cf6",
borderHover: "#a78bfa",
},
emerald: {
fill: "#10b981",
text: "#6ee7b7",
textHover: "#fff",
border: "#10b981",
borderHover: "#34d399",
},
rose: {
fill: "#f43f5e",
text: "#fda4af",
textHover: "#fff",
border: "#f43f5e",
borderHover: "#fb7185",
},
"slide-right": {
fill: "#f1f5f9",
text: "#e2e8f0",
textHover: "#0a0a0a",
border: "#475569",
borderHover: "#f1f5f9",
},
curtain: {
fill: "#8b5cf6",
text: "#c4b5fd",
textHover: "#fff",
border: "#8b5cf6",
borderHover: "#a78bfa",
},
glow: {
fill: "#3b82f6",
text: "#93c5fd",
textHover: "#fff",
border: "#3b82f6",
borderHover: "#60a5fa",
},
};
const sizeStyles = {
sm: "padding: 0.625rem 1.5rem; font-size: 0.8125rem;",
md: "padding: 0.875rem 2rem; font-size: 0.9375rem;",
lg: "padding: 1.125rem 2.75rem; font-size: 1.0625rem;",
};
let hoverStates = {};
function setHover(id, val) {
hoverStates[id] = val;
hoverStates = hoverStates;
}
const buttons = [
{ id: "a", variant: "default", size: "md", label: "Explore" },
{ id: "b", variant: "blue", size: "md", label: "Get Started" },
{ id: "c", variant: "purple", size: "md", label: "Learn More" },
{ id: "d", variant: "emerald", size: "lg", label: "Download Now" },
{ id: "e", variant: "rose", size: "sm", label: "Sign Up" },
{ id: "f", variant: "slide-right", size: "md", label: "Slide Right" },
{ id: "g", variant: "curtain", size: "md", label: "Curtain Reveal" },
{ id: "h", variant: "glow", size: "md", label: "Glow Effect" },
];
function btnStyle(variant, size, hovered) {
const config = variantConfig[variant];
const isGlow = variant === "glow";
const boxShadow =
isGlow && hovered ? "0 0 20px rgba(59,130,246,0.4), 0 0 40px rgba(59,130,246,0.2)" : "none";
return `position:relative;display:inline-flex;align-items:center;justify-content:center;border-radius:10px;font-weight:600;letter-spacing:0.01em;cursor:pointer;background:transparent;border:1.5px solid ${hovered ? config.borderHover : config.border};outline:none;overflow:hidden;transition:border-color 0.35s ease,box-shadow 0.35s ease;box-shadow:${boxShadow};${sizeStyles[size]}`;
}
function fillStyle(variant, hovered) {
const config = variantConfig[variant];
const isSlideRight = variant === "slide-right";
const isCurtain = variant === "curtain";
if (isSlideRight) {
return `position:absolute;top:0;left:0;width:${hovered ? "100%" : "0"};height:100%;background:${config.fill};transition:width 0.35s cubic-bezier(0.25,0.46,0.45,0.94);z-index:0;`;
}
if (isCurtain) {
return `position:absolute;top:50%;left:0;width:100%;height:${hovered ? "100%" : "0"};transform:translateY(-50%);background:${config.fill};transition:height 0.35s cubic-bezier(0.25,0.46,0.45,0.94);z-index:0;`;
}
return `position:absolute;bottom:0;left:0;width:100%;height:${hovered ? "100%" : "0"};background:${config.fill};transition:height 0.35s cubic-bezier(0.25,0.46,0.45,0.94);z-index:0;`;
}
function labelStyle(variant, hovered) {
const config = variantConfig[variant];
return `position:relative;z-index:1;color:${hovered ? config.textHover : config.text};transition:color 0.35s ease;`;
}
</script>
<div class="hover-btn-demo">
<h2 class="demo-title">Interactive Hover Buttons</h2>
<p class="demo-subtitle">Hover over the buttons to see the fill effect</p>
<div class="btn-row">
{#each buttons.slice(0, 3) as btn}
<button
style={btnStyle(btn.variant, btn.size, hoverStates[btn.id])}
on:mouseenter={() => setHover(btn.id, true)}
on:mouseleave={() => setHover(btn.id, false)}
>
<span style={fillStyle(btn.variant, hoverStates[btn.id])}></span>
<span style={labelStyle(btn.variant, hoverStates[btn.id])}>{btn.label}</span>
</button>
{/each}
</div>
<div class="btn-row">
{#each buttons.slice(3, 5) as btn}
<button
style={btnStyle(btn.variant, btn.size, hoverStates[btn.id])}
on:mouseenter={() => setHover(btn.id, true)}
on:mouseleave={() => setHover(btn.id, false)}
>
<span style={fillStyle(btn.variant, hoverStates[btn.id])}></span>
<span style={labelStyle(btn.variant, hoverStates[btn.id])}>{btn.label}</span>
</button>
{/each}
</div>
<div class="btn-row">
{#each buttons.slice(5) as btn}
<button
style={btnStyle(btn.variant, btn.size, hoverStates[btn.id])}
on:mouseenter={() => setHover(btn.id, true)}
on:mouseleave={() => setHover(btn.id, false)}
>
<span style={fillStyle(btn.variant, hoverStates[btn.id])}></span>
<span style={labelStyle(btn.variant, hoverStates[btn.id])}>{btn.label}</span>
</button>
{/each}
</div>
</div>
<style>
.hover-btn-demo {
min-height: 100vh;
background: #0a0a0a;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 2rem;
font-family: system-ui, -apple-system, sans-serif;
}
.demo-title {
font-size: 1.5rem;
font-weight: 700;
color: #e2e8f0;
margin: 0;
}
.demo-subtitle {
color: #525252;
font-size: 0.875rem;
margin: 0;
}
.btn-row {
display: flex;
flex-wrap: wrap;
gap: 2rem;
justify-content: center;
}
</style>Interactive Hover Button
A button with multi-layered hover effects that combine a background fill sliding up from the bottom, text color inversion, and a subtle border animation for maximum visual impact.
How it works
- A
::beforepseudo-element sits atbottom: 0withheight: 0 - On
:hover, the pseudo-element’s height transitions to100%, filling from bottom to top - The text color transitions from light to dark (or vice versa) for contrast inversion
- The border color animates to complement the filled background
Variants
- Default: White fill with dark text on hover
- Colored: Colored fills (blue, purple, emerald) with white text on hover
- Outline-to-fill: Starts as an outline button and fills solid on hover
When to use it
- Navigation links styled as buttons
- Secondary CTAs that should feel interactive
- Any button where a satisfying hover state improves the experience