Browse the library
Components / Buttons
Shine button
FreePill button with a soft light sweep on hover, three variants, three sizes and an arrow that nudges forward.
View as Markdownlucide-react
components/facade/shine-button.tsx
import * as React from "react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
export type ShineButtonVariant = "brand" | "solid" | "outline";
export type ShineButtonSize = "sm" | "md" | "lg";
export interface ShineButtonBaseProps {
/** Visual style. Defaults to "brand". */
variant?: ShineButtonVariant;
/** Height and padding scale. Defaults to "md". */
size?: ShineButtonSize;
/** Optional leading icon. */
icon?: React.ReactNode;
/** Show a trailing arrow that nudges forward on hover. */
arrow?: boolean;
className?: string;
children?: React.ReactNode;
}
type ShineButtonAsButton = ShineButtonBaseProps &
Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof ShineButtonBaseProps> & {
href?: undefined;
};
type ShineButtonAsAnchor = ShineButtonBaseProps &
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof ShineButtonBaseProps> & {
href: string;
};
/** Pass `href` to render an `<a>`, otherwise a `<button>` is rendered. */
export type ShineButtonProps = ShineButtonAsButton | ShineButtonAsAnchor;
const variantClasses: Record<ShineButtonVariant, string> = {
brand: cn(
"bg-brand text-brand-foreground",
"shadow-[0_1px_2px_0_color-mix(in_oklch,var(--brand)_35%,transparent),0_10px_28px_-10px_color-mix(in_oklch,var(--brand)_70%,transparent)]",
"hover:shadow-[0_1px_2px_0_color-mix(in_oklch,var(--brand)_35%,transparent),0_14px_34px_-10px_color-mix(in_oklch,var(--brand)_85%,transparent)]",
"before:shadow-[inset_0_1px_0_0_color-mix(in_oklch,var(--brand-foreground)_28%,transparent)]",
),
solid: cn(
"bg-primary text-primary-foreground",
"shadow-[0_1px_2px_0_color-mix(in_oklch,var(--foreground)_18%,transparent),0_10px_24px_-12px_color-mix(in_oklch,var(--foreground)_45%,transparent)]",
"before:shadow-[inset_0_1px_0_0_color-mix(in_oklch,var(--primary-foreground)_18%,transparent)]",
),
outline: cn(
"border border-border bg-background/70 text-foreground backdrop-blur",
"shadow-[0_1px_2px_0_color-mix(in_oklch,var(--foreground)_6%,transparent)]",
"hover:border-foreground/20 hover:bg-muted/60",
),
};
const shineClasses: Record<ShineButtonVariant, string> = {
brand: "via-brand-foreground/40",
solid: "via-primary-foreground/25",
outline: "via-brand/20",
};
const sizeClasses: Record<ShineButtonSize, string> = {
sm: "h-8 gap-1.5 px-3.5 text-[13px] [&_svg]:size-3.5",
md: "h-10 gap-2 px-5 text-sm [&_svg]:size-4",
lg: "h-12 gap-2.5 px-6 text-[15px] [&_svg]:size-[18px]",
};
export function ShineButton(props: ShineButtonProps) {
const { variant = "brand", size = "md", icon, arrow = false, className, children } = props;
const classes = cn(
"group relative isolate inline-flex shrink-0 select-none items-center justify-center overflow-hidden whitespace-nowrap rounded-full font-medium tracking-[-0.01em]",
"transition-[box-shadow,background-color,border-color,transform] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] active:scale-[0.98]",
"before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit]",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
variantClasses[variant],
sizeClasses[size],
className,
);
const inner = (
<>
<span aria-hidden className="pointer-events-none absolute inset-0 -z-10 overflow-hidden rounded-[inherit]">
<span
className={cn(
"absolute inset-y-0 -left-1/2 w-1/2 -skew-x-[20deg] bg-linear-to-r from-transparent to-transparent opacity-0",
"transition-none group-hover:translate-x-[320%] group-hover:opacity-100 group-hover:transition-[transform,opacity] group-hover:duration-[900ms] group-hover:ease-[cubic-bezier(0.22,1,0.36,1)]",
"group-focus-visible:translate-x-[320%] group-focus-visible:opacity-100 group-focus-visible:transition-[transform,opacity] group-focus-visible:duration-[900ms]",
shineClasses[variant],
)}
/>
</span>
{icon ? <span className="inline-flex shrink-0 items-center">{icon}</span> : null}
{children ? <span className="relative">{children}</span> : null}
{arrow ? (
<ArrowRight
aria-hidden
className="shrink-0 transition-transform duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-x-1"
/>
) : null}
</>
);
if (props.href !== undefined) {
const {
variant: _variant,
size: _size,
icon: _icon,
arrow: _arrow,
className: _className,
children: _children,
...anchorProps
} = props;
return (
<a {...anchorProps} className={classes}>
{inner}
</a>
);
}
const {
variant: _variant,
size: _size,
icon: _icon,
arrow: _arrow,
className: _className,
children: _children,
href: _href,
type = "button",
...buttonProps
} = props;
return (
<button {...buttonProps} type={type} className={classes}>
{inner}
</button>
);
}
export default ShineButton;app/page.tsx
import { Phone, Shield, Star } from "lucide-react";
import { ShineButton } from "@/components/facade/shine-button";
export default function Demo() {
return (
<div className="w-full max-w-2xl">
<div className="relative overflow-hidden rounded-2xl border border-border bg-card p-7 text-card-foreground shadow-[0_1px_2px_0_color-mix(in_oklch,var(--foreground)_5%,transparent),0_24px_48px_-24px_color-mix(in_oklch,var(--foreground)_18%,transparent)] sm:p-10">
<div
aria-hidden
className="pointer-events-none absolute -right-24 -top-24 size-72 rounded-full bg-brand/15 blur-3xl"
/>
<div className="relative">
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-background/70 px-2.5 py-1 text-xs font-medium text-muted-foreground">
<span className="size-1.5 rounded-full bg-success" />
Booking spring inspections in Denver
</span>
<h3 className="mt-4 max-w-md font-display text-3xl font-semibold tracking-tight sm:text-[34px] sm:leading-[1.1]">
Get a free roof inspection before the hail season hits.
</h3>
<p className="mt-3 max-w-md text-[15px] leading-relaxed text-muted-foreground">
A licensed inspector on your roof within 48 hours, with photos and a written report you can send to your insurer.
</p>
<div className="mt-7 flex flex-wrap items-center gap-3">
<ShineButton size="lg" arrow>
Book my free inspection
</ShineButton>
<ShineButton size="lg" variant="outline" href="tel:+13035550142" icon={<Phone aria-hidden />}>
(303) 555-0142
</ShineButton>
</div>
<div className="mt-8 flex flex-wrap items-center gap-x-6 gap-y-3 border-t border-border pt-6 text-[13px] text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<Star className="size-3.5 fill-warning text-warning" aria-hidden />
4.9 from 812 Google reviews
</span>
<span className="inline-flex items-center gap-1.5">
<Shield className="size-3.5 text-foreground/70" aria-hidden />
Licensed and insured in Colorado
</span>
<div className="ml-auto hidden items-center gap-2 sm:flex">
<ShineButton size="sm" variant="solid">
Solid
</ShineButton>
<ShineButton size="sm" variant="outline">
Outline
</ShineButton>
</div>
</div>
</div>
</div>
</div>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/shine-button.jsonUsage
import { ShineButton } from "@/components/facade/shine-button";
export default function Page() {
return <ShineButton />;
}Props
ShineButtonProps is exported, so you can extend or wrap it. Every prop has a default; <ShineButton /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| variant | ShineButtonVariant | Visual style. Defaults to "brand". |
| size | ShineButtonSize | Height and padding scale. Defaults to "md". |
| icon | React.ReactNode | Optional leading icon. |
| arrow | boolean | Show a trailing arrow that nudges forward on hover. |
| className | string | |
| children | React.ReactNode |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade shine button to this page and adapt the copy to our business.