gaia
uiraised-buttonsource: ok · hit
Raised Button
A beautiful 3D raised button with customizable colors and dynamic text contrast.
npx shadcn add "https://ui.heygaia.io/r/raised-button.json"free
Visual style
3d
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightclass-variance-authority
Files2 · source cached 10/14/2026, 2:13:53 PM
registry/new-york/ui/raised-button.tsx
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
getContrastColor,
getLuminance,
parseColor,
} from "@/lib/utils/colorUtils";
const buttonVariants = cva(
"inline-flex items-center justify-center dark:bg-zinc-500 dark:text-white whitespace-nowrap text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 relative bg-primary text-primary-foreground hover:bg-primary/90 border border-primary/50 shadow-md before:absolute before:inset-0 before:border-t before:border-white/40 before:bg-gradient-to-b before:from-white/20 before:to-transparent cursor-pointer transition-transform duration-200 active:scale-[0.96] subpixel-antialiased gap-2",
{
variants: {
variant: {
default: "",
},
size: {
default: "h-10 px-4 py-2 rounded-xl before:rounded-xl",
sm: "h-9 rounded-lg px-3 before:rounded-xl",
lg: "h-11 rounded-lg px-8 before:rounded-lg",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
color?: string;
}
const RaisedButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, color, style = {}, ...props }, ref) => {
const Comp = "button";
const dynamicStyles = React.useMemo(() => {
if (!color) return {};
try {
const rgb = parseColor(color);
if (!rgb) return {};
const luminance = getLuminance(rgb);
const textColor = getContrastColor(luminance);
const borderOpacity = 0.5;
const hoverOpacity = 0.9;
const whiteBorderOpacity = 0.6;
const whiteGradientOpacity = 0.3;
const shadowOpacity = 0.2;
const shadowSpread = "0px";
const shadowBlur = "5px";
return {
backgroundColor: color,
color: textColor,
borderColor: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${borderOpacity})`,
"--hover-bg": `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${hoverOpacity})`,
"--border": `rgba(255, 255, 255, ${whiteBorderOpacity})`,
"--gradient": `rgba(255, 255, 255, ${whiteGradientOpacity})`,
"--shadow-color": `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${shadowOpacity})`,
boxShadow: `0 4px ${shadowBlur} ${shadowSpread} var(--shadow-color)`,
transition: "all 0.2s ease-in-out",
};
} catch (e) {
console.error("Error processing color:", e);
return {};
}
}, [color]);
const computedClassName = cn(
buttonVariants({ variant, size, className }),
color &&
"hover:bg-[color:var(--hover-bg)] before:border-[color:var(--border)] before:from-[color:var(--gradient)] hover:opacity-80 overflow-hidden",
);
return (
<Comp
className={computedClassName}
ref={ref}
style={{
...style,
...dynamicStyles,
}}
{...props}
/>
);
},
);
RaisedButton.displayName = "RaisedButton";
export { buttonVariants, RaisedButton };
lib/utils/colorUtils.ts
const CSS_COLOR_NAMES: Record<string, [number, number, number]> = {
black: [0, 0, 0],
white: [255, 255, 255],
red: [255, 0, 0],
green: [0, 128, 0],
blue: [0, 0, 255],
yellow: [255, 255, 0],
cyan: [0, 255, 255],
magenta: [255, 0, 255],
gray: [128, 128, 128],
grey: [128, 128, 128],
orange: [255, 165, 0],
purple: [128, 0, 128],
pink: [255, 192, 203],
brown: [165, 42, 42],
navy: [0, 0, 128],
teal: [0, 128, 128],
olive: [128, 128, 0],
maroon: [128, 0, 0],
silver: [192, 192, 192],
lime: [0, 255, 0],
aqua: [0, 255, 255],
fuchsia: [255, 0, 255],
};
export function hexToRgb(
hex: string,
): { r: number; g: number; b: number } | null {
const normalizedHex = hex.charAt(0) === "#" ? hex.substring(1) : hex;
if (normalizedHex.length === 3) {
const r = parseInt(normalizedHex.charAt(0) + normalizedHex.charAt(0), 16);
const g = parseInt(normalizedHex.charAt(1) + normalizedHex.charAt(1), 16);
const b = parseInt(normalizedHex.charAt(2) + normalizedHex.charAt(2), 16);
return { r, g, b };
}
if (normalizedHex.length === 6) {
const r = parseInt(normalizedHex.substring(0, 2), 16);
const g = parseInt(normalizedHex.substring(2, 4), 16);
const b = parseInt(normalizedHex.substring(4, 6), 16);
return { r, g, b };
}
if (normalizedHex !== hex) {
try {
const tempElement = document.createElement("div");
tempElement.style.color = hex;
document.body.appendChild(tempElement);
const computedColor = window.getComputedStyle(tempElement).color;
document.body.removeChild(tempElement);
const rgbMatch = computedColor.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/,
);
if (rgbMatch) {
return {
r: parseInt(rgbMatch[1], 10),
g: parseInt(rgbMatch[2], 10),
b: parseInt(rgbMatch[3], 10),
};
}
} catch (e) {
console.error("Error processing color name:", e);
}
}
return null;
}
export function getLuminance(rgb: { r: number; g: number; b: number }): number {
const { r, g, b } = rgb;
const sR = r / 255;
const sG = g / 255;
const sB = b / 255;
const R = sR <= 0.03928 ? sR / 12.92 : ((sR + 0.055) / 1.055) ** 2.4;
const G = sG <= 0.03928 ? sG / 12.92 : ((sG + 0.055) / 1.055) ** 2.4;
const B = sB <= 0.03928 ? sB / 12.92 : ((sB + 0.055) / 1.055) ** 2.4;
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
export function getContrastColor(luminance: number): string {
return luminance > 0.5 ? "#000000" : "#ffffff";
}
export function parseColor(
color: string,
): { r: number; g: number; b: number } | null {
const hexResult = hexToRgb(color);
if (hexResult) return hexResult;
try {
if (typeof document !== "undefined") {
const tempElement = document.createElement("div");
tempElement.style.color = color;
document.body.appendChild(tempElement);
const computedColor = window.getComputedStyle(tempElement).color;
document.body.removeChild(tempElement);
const rgbMatch = computedColor.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/,
);
if (rgbMatch) {
return {
r: parseInt(rgbMatch[1], 10),
g: parseInt(rgbMatch[2], 10),
b: parseInt(rgbMatch[3], 10),
};
}
} else {
const colorMap: Record<string, { r: number; g: number; b: number }> = {};
for (const [name, rgb] of Object.entries(CSS_COLOR_NAMES)) {
const [r, g, b] = rgb;
colorMap[name.toLowerCase()] = { r, g, b };
}
const normalizedColorName = color.toLowerCase().trim();
if (colorMap[normalizedColorName]) {
return colorMap[normalizedColorName];
}
}
const rgbDirectMatch = color.match(
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/,
);
if (rgbDirectMatch) {
return {
r: parseInt(rgbDirectMatch[1], 10),
g: parseInt(rgbDirectMatch[2], 10),
b: parseInt(rgbDirectMatch[3], 10),
};
}
const hslMatch = color.match(
/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%(?:,\s*[\d.]+)?\)/,
);
if (hslMatch) {
const h = parseInt(hslMatch[1], 10) / 360;
const s = parseInt(hslMatch[2], 10) / 100;
const l = parseInt(hslMatch[3], 10) / 100;
return hslToRgb(h, s, l);
}
} catch (e) {
console.error("Error processing color:", e);
}
return null;
}
export function hslToRgb(
h: number,
s: number,
l: number,
): { r: number; g: number; b: number } {
let r: number, g: number, b: number;
if (s === 0) {
r = g = b = l * 255;
} else {
const hue2rgb = (p: number, q: number, t: number): number => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3) * 255;
g = hue2rgb(p, q, h) * 255;
b = hue2rgb(p, q, h - 1 / 3) * 255;
}
return {
r: Math.round(r),
g: Math.round(g),
b: Math.round(b),
};
}