"use client";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
type AnchorHTMLAttributes,
type ButtonHTMLAttributes,
cloneElement,
type FocusEventHandler,
forwardRef,
isValidElement,
type KeyboardEvent,
type KeyboardEventHandler,
memo,
type PointerEvent,
type PointerEventHandler,
type ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
const componentThemeClassName =
"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]";
const buttonThemeClassName =
"[--button-destructive:#dc2626] [--button-input:#e3e7ec] [--button-primary-foreground:#ffffff] [--button-ring:rgba(17,17,17,0.16)] [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] [--button-secondary:#eceff3] [--button-secondary-foreground:#111111] dark:[--button-destructive:#f87171] dark:[--button-input:#2b2a25] dark:[--button-primary-foreground:#111111] dark:[--button-ring:rgba(246,243,236,0.18)] dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_30%)] dark:[--button-secondary:#2a2a27] dark:[--button-secondary-foreground:#f6f3ec]";
const buttonCornerClassName =
"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]";
const buttonCornerXsClassName =
"rounded-[min(var(--radius-md),10px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[12px]";
const buttonCornerSmClassName =
"rounded-[min(var(--radius-md),12px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[14px]";
const buttonGroupCornerClassName =
"in-data-[slot=button-group]:rounded-lg in-data-[slot=button-group]:supports-[corner-shape:squircle]:corner-squircle in-data-[slot=button-group]:supports-[corner-shape:squircle]:rounded-[11px]";
const buttonVariants = cva(
cn(
componentThemeClassName,
buttonThemeClassName,
buttonCornerClassName,
"group/button relative isolate inline-flex shrink-0 cursor-pointer touch-manipulation select-none items-center justify-center overflow-hidden whitespace-nowrap border border-transparent bg-clip-border font-medium text-sm no-underline outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:border-[color:var(--button-ring)] focus-visible:ring-3 focus-visible:ring-[color:color-mix(in_oklch,var(--button-ring),transparent_50%)] active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-[color:var(--button-destructive)] aria-invalid:ring-3 aria-invalid:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:aria-invalid:border-[color:color-mix(in_oklch,var(--button-destructive),transparent_50%)] dark:aria-invalid:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
),
{
variants: {
variant: {
default:
"visited:!text-primary-foreground hover:!text-primary-foreground focus-visible:!text-primary-foreground active:!text-primary-foreground bg-primary text-primary-foreground [--button-ripple:color-mix(in_oklch,var(--primary-foreground),transparent_45%)] hover:bg-primary dark:[--button-ripple:color-mix(in_oklch,#111111,transparent_10%)] [&_svg]:text-primary-foreground",
outline:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground aria-expanded:!text-foreground border-border bg-background text-foreground [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] hover:bg-accent/60 aria-expanded:bg-muted dark:border-[color:var(--button-input)] dark:bg-[color-mix(in_oklch,var(--button-input),transparent_70%)] dark:hover:bg-[color-mix(in_oklch,var(--button-input),transparent_50%)] dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_28%)] [&_svg]:text-foreground",
secondary:
"visited:!text-[color:var(--button-secondary-foreground)] hover:!text-[color:var(--button-secondary-foreground)] focus-visible:!text-[color:var(--button-secondary-foreground)] active:!text-[color:var(--button-secondary-foreground)] aria-expanded:!text-[color:var(--button-secondary-foreground)] bg-[color:var(--button-secondary)] text-[color:var(--button-secondary-foreground)] [--button-ripple:color-mix(in_oklch,var(--button-secondary-foreground),transparent_52%)] hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_7%)] aria-expanded:bg-[color:var(--button-secondary)] dark:hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_11%)] dark:[--button-ripple:color-mix(in_oklch,var(--button-secondary-foreground),transparent_20%)] [&_svg]:text-[color:var(--button-secondary-foreground)]",
ghost:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground aria-expanded:!text-foreground text-foreground [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] hover:bg-accent/60 aria-expanded:bg-muted dark:hover:bg-muted/50 dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_28%)] [&_svg]:text-foreground",
destructive:
"visited:!text-[color:var(--button-destructive)] hover:!text-[color:var(--button-destructive)] focus-visible:!text-[color:var(--button-destructive)] active:!text-[color:var(--button-destructive)] bg-[color-mix(in_oklch,var(--button-destructive),transparent_90%)] text-[color:var(--button-destructive)] [--button-ripple:color-mix(in_oklch,var(--button-destructive),transparent_40%)] hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] focus-visible:border-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] dark:hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_70%)] dark:[--button-ripple:color-mix(in_oklch,var(--button-destructive),transparent_20%)] [&_svg]:text-[color:var(--button-destructive)]",
link: "",
},
linkUnderline: {
motion: "",
static: "",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: cn(
"h-6 gap-1 px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
buttonGroupCornerClassName,
buttonCornerXsClassName
),
sm: cn(
"h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
buttonGroupCornerClassName,
buttonCornerSmClassName
),
lg: "h-10 gap-1.5 px-4 text-base leading-5 has-data-[icon=inline-end]:pr-3.5 has-data-[icon=inline-start]:pl-3.5",
icon: "size-8",
"icon-xs": cn(
"size-6 [&_svg:not([class*='size-'])]:size-3",
buttonGroupCornerClassName,
buttonCornerXsClassName
),
"icon-sm": cn(
"size-7",
buttonGroupCornerClassName,
buttonCornerSmClassName
),
"icon-lg": "size-9",
},
},
compoundVariants: [
{
variant: "link",
linkUnderline: "motion",
class:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground px-0 text-foreground shadow-none active:translate-y-0 [&_[data-link-label]]:font-medium [&_[data-link-label]]:text-[1em]",
},
{
variant: "link",
linkUnderline: "static",
class:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground px-0 text-foreground underline-offset-4 hover:underline [&_[data-link-label]]:font-medium [&_[data-link-label]]:text-[1em]",
},
{
variant: "link",
size: "default",
class: "h-8 px-0 text-sm leading-5",
},
{
variant: "link",
size: "lg",
class: "h-10 px-0 text-base leading-5",
},
{
variant: "link",
size: "sm",
class: "h-7 px-0 text-[0.8rem]",
},
{
variant: "link",
size: "xs",
class: "h-6 px-0 text-xs",
},
],
defaultVariants: {
variant: "default",
size: "default",
linkUnderline: "motion",
},
}
);
const linkUnderlineFillTransition = {
type: "spring" as const,
stiffness: 420,
damping: 36,
mass: 0.65,
};
const MAX_RIPPLES = 3;
const REL_TOKEN_SPLIT = /\s+/;
const LinkUnderlineLabel = memo(function LinkUnderlineLabel({
children,
enabled,
}: {
children: ReactNode;
enabled: boolean;
}) {
if (!enabled) {
return <>{children}</>;
}
return (
<span className="relative inline-block pb-px font-medium text-[1em] leading-[inherit]">
<span className="relative z-10" data-link-label>
{children}
</span>
<span
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-px bg-muted-foreground/50"
/>
<motion.span
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-px origin-left bg-foreground"
transition={linkUnderlineFillTransition}
variants={{
rest: { scaleX: 0 },
hover: { scaleX: 1 },
}}
/>
</span>
);
});
const buttonContentClassName =
"relative z-10 inline-flex items-center justify-center [gap:inherit] whitespace-nowrap text-inherit";
type Ripple = { id: string; x: number; y: number; size: number };
type AnchorHTMLAttributesForMotion = Omit<
AnchorHTMLAttributes<HTMLAnchorElement>,
| "onAnimationEnd"
| "onAnimationIteration"
| "onAnimationStart"
| "onDrag"
| "onDragEnd"
| "onDragEnter"
| "onDragExit"
| "onDragLeave"
| "onDragOver"
| "onDragStart"
| "onDrop"
>;
type ButtonHTMLAttributesForMotion = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
| "onAnimationEnd"
| "onAnimationIteration"
| "onAnimationStart"
| "onDrag"
| "onDragEnd"
| "onDragEnter"
| "onDragExit"
| "onDragLeave"
| "onDragOver"
| "onDragStart"
| "onDrop"
>;
type ButtonRenderProps = React.HTMLAttributes<HTMLButtonElement> & {
className?: string;
ref?: React.Ref<HTMLButtonElement>;
style?: React.CSSProperties;
};
function setRef<T>(ref: React.Ref<T> | undefined, value: T) {
if (typeof ref === "function") {
ref(value);
return;
}
if (ref) {
(ref as React.MutableRefObject<T>).current = value;
}
}
function useMeasure<T extends HTMLElement = HTMLElement>(
enabled: boolean
): [(node: T | null) => void, { width: number; height: number }] {
const [element, setElement] = useState<T | null>(null);
const [bounds, setBounds] = useState({ width: 0, height: 0 });
const ref = useCallback((node: T | null) => {
setElement(node);
}, []);
useEffect(() => {
if (!(enabled && element && typeof ResizeObserver !== "undefined")) {
return;
}
const observer = new ResizeObserver(([entry]) => {
setBounds((current) => {
const next = {
width: entry.contentRect.width,
height: entry.contentRect.height,
};
if (current.width === next.width && current.height === next.height) {
return current;
}
return next;
});
});
observer.observe(element);
return () => observer.disconnect();
}, [element, enabled]);
return [ref, enabled ? bounds : { width: 0, height: 0 }];
}
const MotionAnchor = motion.create("a");
const buttonTransition = {
scale: {
type: "spring",
stiffness: 640,
damping: 38,
mass: 0.85,
},
width: {
type: "spring",
stiffness: 420,
damping: 34,
mass: 0.9,
},
opacity: {
type: "spring",
stiffness: 380,
damping: 32,
mass: 0.8,
},
y: { type: "spring", stiffness: 340, damping: 28, mass: 0.8 },
} as const;
const loadingIconTransition = {
type: "spring" as const,
stiffness: 400,
damping: 30,
mass: 0.76,
};
const loadingLabelExitTransition = {
type: "spring" as const,
stiffness: 420,
damping: 38,
mass: 0.84,
};
const loadingLabelEnterTransition = {
type: "spring" as const,
stiffness: 280,
damping: 30,
mass: 0.98,
};
const loadingLayoutTransition = {
type: "spring" as const,
stiffness: 360,
damping: 36,
mass: 0.94,
};
const loadingLabelVariants = {
animate: {
filter: "blur(0px)",
opacity: 1,
scale: 1,
transition: loadingLabelEnterTransition,
y: 0,
},
exit: {
filter: "blur(3px)",
opacity: 0,
scale: 0.96,
transition: loadingLabelExitTransition,
y: -4,
},
initial: {
filter: "blur(3px)",
opacity: 0,
scale: 0.96,
y: 4,
},
} as const;
function getLabelMotionKey(children: ReactNode, loading: boolean) {
if (typeof children === "string" || typeof children === "number") {
return `${loading ? "busy" : "idle"}:${children}`;
}
return loading ? "busy" : "idle";
}
function withLoadingSpin(node: ReactNode, spinning: boolean) {
if (!(spinning && isValidElement<{ className?: string }>(node))) {
return node;
}
return cloneElement(node, {
className: cn(node.props.className, "animate-spin"),
});
}
function getButtonAnimate(
canAnimate: boolean,
isPressed: boolean,
animatedWidth: number | undefined,
reduceMotion: boolean | null
) {
const shouldReduceMotion = reduceMotion === true;
const pressScale = canAnimate && isPressed && !shouldReduceMotion ? 0.96 : 1;
if (animatedWidth) {
return {
scale: pressScale,
width: animatedWidth,
};
}
return {
scale: pressScale,
};
}
function omitLinkMotionConflicts<T extends Record<string, unknown>>(props: T) {
const {
onAnimationEnd: _onAnimationEnd,
onAnimationIteration: _onAnimationIteration,
onAnimationStart: _onAnimationStart,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
onDragEnter: _onDragEnter,
onDragExit: _onDragExit,
onDragLeave: _onDragLeave,
onDragOver: _onDragOver,
onDragStart: _onDragStart,
onDrop: _onDrop,
...rest
} = props;
return rest;
}
function resolvePrimitiveButtonProps(buttonProps: ButtonRenderProps) {
const {
children: _buttonChildren,
className: primitiveClassName,
onAnimationEnd: _onAnimationEnd,
onAnimationIteration: _onAnimationIteration,
onAnimationStart: _onAnimationStart,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
onDragEnter: _onDragEnter,
onDragExit: _onDragExit,
onDragLeave: _onDragLeave,
onDragOver: _onDragOver,
onDragStart: _onDragStart,
onDrop: _onDrop,
ref: primitiveRef,
style: primitiveStyle,
...resolvedButtonProps
} = buttonProps;
return {
primitiveClassName,
primitiveRef,
primitiveStyle,
resolvedButtonProps,
};
}
function resolveAnchorRel(target?: string, rel?: string) {
if (!target?.includes("_blank")) {
return rel;
}
const tokens = new Set((rel ?? "").split(REL_TOKEN_SPLIT).filter(Boolean));
tokens.add("noopener");
tokens.add("noreferrer");
return [...tokens].join(" ");
}
function isIconOnlySize(
size:
| "default"
| "xs"
| "sm"
| "lg"
| "icon"
| "icon-xs"
| "icon-sm"
| "icon-lg"
| null
| undefined
) {
return (
size === "icon" ||
size === "icon-xs" ||
size === "icon-sm" ||
size === "icon-lg"
);
}
function useIconOnlyA11yWarning(
size:
| "default"
| "xs"
| "sm"
| "lg"
| "icon"
| "icon-xs"
| "icon-sm"
| "icon-lg"
| null
| undefined,
children: ReactNode,
iconLabel: string | undefined,
ariaLabel: string | undefined,
loading: boolean
) {
useEffect(() => {
if (process.env.NODE_ENV !== "development") {
return;
}
const hasVisibleLabel =
children != null && children !== false && children !== "";
const needsAccessibleName =
isIconOnlySize(size) || (loading && !hasVisibleLabel);
if (!needsAccessibleName || iconLabel || ariaLabel) {
return;
}
console.warn(
"[Button] Icon-only and loading-only buttons need `iconLabel` or `aria-label` for accessibility."
);
}, [ariaLabel, children, iconLabel, loading, size]);
}
function getIconIdleAnimate(showSpinner: boolean, noMotion: boolean) {
if (noMotion) {
return { opacity: showSpinner ? 0 : 1 };
}
return {
filter: showSpinner ? "blur(3px)" : "blur(0px)",
opacity: showSpinner ? 0 : 1,
rotate: showSpinner ? -56 : 0,
scale: showSpinner ? 0.82 : 1,
};
}
function getIconSpinnerAnimate(showSpinner: boolean, noMotion: boolean) {
if (noMotion) {
return { opacity: showSpinner ? 1 : 0 };
}
return {
filter: showSpinner ? "blur(0px)" : "blur(3px)",
opacity: showSpinner ? 1 : 0,
rotate: showSpinner ? 0 : 56,
scale: showSpinner ? 1 : 0.82,
};
}
function buildMotionSurfaceProps({
canAnimate,
isPressed,
motionAnimate,
reduceMotion,
useLinkMotionUnderline,
variant,
}: {
canAnimate: boolean;
isPressed: boolean;
motionAnimate: ReturnType<typeof getButtonAnimate>;
reduceMotion: boolean | null;
useLinkMotionUnderline: boolean;
variant: VariantProps<typeof buttonVariants>["variant"];
}) {
const shouldReduceMotion = reduceMotion === true;
const surfaceTransition = shouldReduceMotion
? { duration: 0 }
: buttonTransition;
const shared = {
animate: motionAnimate,
"data-pressed": isPressed ? "true" : "false",
transition: surfaceTransition,
};
if (useLinkMotionUnderline) {
return {
...shared,
initial: shouldReduceMotion ? (false as const) : ("rest" as const),
variants: shouldReduceMotion ? undefined : { rest: {}, hover: {} },
whileHover: shouldReduceMotion ? undefined : ("hover" as const),
};
}
const hoverLift =
canAnimate && variant !== "link" && !shouldReduceMotion
? { y: -1 }
: undefined;
return {
...shared,
initial: false as const,
whileHover: hoverLift,
};
}
const ButtonIconSlot = memo(function ButtonIconSlot({
accessibleName,
dataIcon,
hasIcon,
hasLabel,
icon,
iconTransition,
layoutTransition,
noMotion,
showSpinner,
spinner,
}: {
accessibleName?: string;
dataIcon: "inline-start" | "inline-end";
hasIcon: boolean;
hasLabel: boolean;
icon?: ReactNode;
iconTransition: { duration: number } | typeof loadingIconTransition;
layoutTransition: { duration: number } | typeof loadingLayoutTransition;
noMotion: boolean;
showSpinner: boolean;
spinner: ReactNode;
}) {
return (
<motion.span
aria-hidden={hasLabel || accessibleName ? true : undefined}
className="relative inline-flex size-[1em] shrink-0 items-center justify-center"
data-icon={dataIcon}
layout={noMotion ? false : "position"}
transition={layoutTransition}
>
{hasIcon ? (
<motion.span
animate={getIconIdleAnimate(showSpinner, noMotion)}
className="absolute inset-0 inline-flex items-center justify-center"
initial={false}
transition={iconTransition}
>
{icon}
</motion.span>
) : null}
<motion.span
animate={getIconSpinnerAnimate(showSpinner, noMotion)}
className="absolute inset-0 inline-flex items-center justify-center"
initial={false}
transition={iconTransition}
>
{withLoadingSpin(spinner, showSpinner && !noMotion)}
</motion.span>
</motion.span>
);
});
const ButtonLoadingLabel = memo(function ButtonLoadingLabel({
children,
layoutTransition,
linkUnderlineEnabled,
loading,
noMotion,
}: {
children: ReactNode;
layoutTransition: { duration: number } | typeof loadingLayoutTransition;
linkUnderlineEnabled: boolean;
loading: boolean;
noMotion: boolean;
}) {
return (
<motion.span
className="relative inline-grid min-w-0 items-center leading-[inherit] [grid-template-columns:max-content] [grid-template-rows:minmax(0,1.25em)]"
layout="position"
transition={layoutTransition}
>
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={noMotion ? { opacity: 1 } : "animate"}
className="col-start-1 row-start-1 inline-flex min-w-0 items-center whitespace-nowrap"
exit={noMotion ? { opacity: 0 } : "exit"}
initial={noMotion ? false : "initial"}
key={getLabelMotionKey(children, loading)}
layout="position"
transition={{ layout: layoutTransition }}
variants={noMotion ? undefined : loadingLabelVariants}
>
<LinkUnderlineLabel enabled={linkUnderlineEnabled}>
{children}
</LinkUnderlineLabel>
</motion.span>
</AnimatePresence>
</motion.span>
);
});
const ButtonSpinnerPresence = memo(function ButtonSpinnerPresence({
iconSlot,
layoutTransition,
noMotion,
visible,
}: {
iconSlot: ReactNode;
layoutTransition: { duration: number } | typeof loadingLayoutTransition;
noMotion: boolean;
visible: boolean;
}) {
return (
<AnimatePresence initial={false} mode="popLayout">
{visible ? (
<motion.span
animate={noMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}
className="inline-flex shrink-0"
exit={
noMotion
? { opacity: 0 }
: {
filter: "blur(2px)",
opacity: 0,
scale: 0.86,
transition: loadingLabelExitTransition,
}
}
initial={noMotion ? false : { opacity: 0, scale: 0.86 }}
key="loading-spinner"
layout="position"
transition={layoutTransition}
>
{iconSlot}
</motion.span>
) : null}
</AnimatePresence>
);
});
const defaultLoadingIcon = <Loader2 aria-hidden strokeWidth={2.25} />;
const ButtonLoadingContent = memo(function ButtonLoadingContent({
accessibleName,
children,
contentRef,
icon,
iconPosition,
linkUnderlineEnabled,
loading,
loadingIcon,
reduceMotion,
}: {
accessibleName?: string;
children: ReactNode;
contentRef?: React.Ref<HTMLSpanElement>;
icon?: ReactNode;
iconPosition: "start" | "end";
linkUnderlineEnabled: boolean;
loading: boolean;
loadingIcon?: ReactNode;
reduceMotion: boolean | null;
}) {
const hasIcon = Boolean(icon);
const hasLabel = children != null && children !== false && children !== "";
const noMotion = reduceMotion === true;
const iconTransition = noMotion ? { duration: 0 } : loadingIconTransition;
const layoutTransition = noMotion ? { duration: 0 } : loadingLayoutTransition;
const spinner = loadingIcon ?? defaultLoadingIcon;
const iconAtStart = loading || (hasIcon && iconPosition === "start");
const iconAtEnd = hasIcon && !loading && iconPosition === "end";
const spinnerOnlyAtStart = loading && !hasIcon;
const iconSlotProps = {
accessibleName,
hasLabel,
iconTransition,
layoutTransition,
noMotion,
spinner,
};
return (
<motion.span
animate={loading && !noMotion ? { opacity: 0.96 } : { opacity: 1 }}
className={buttonContentClassName}
initial={false}
layout={!noMotion}
ref={contentRef}
transition={layoutTransition}
>
<ButtonSpinnerPresence
iconSlot={
<ButtonIconSlot
{...iconSlotProps}
dataIcon="inline-start"
hasIcon={hasIcon}
icon={icon}
showSpinner
/>
}
layoutTransition={layoutTransition}
noMotion={noMotion}
visible={spinnerOnlyAtStart}
/>
{hasIcon && iconAtStart ? (
<ButtonIconSlot
{...iconSlotProps}
dataIcon="inline-start"
hasIcon={hasIcon}
icon={icon}
showSpinner={loading}
/>
) : null}
{hasLabel ? (
<ButtonLoadingLabel
layoutTransition={layoutTransition}
linkUnderlineEnabled={linkUnderlineEnabled}
loading={loading}
noMotion={noMotion}
>
{children}
</ButtonLoadingLabel>
) : null}
{hasIcon && iconAtEnd ? (
<ButtonIconSlot
{...iconSlotProps}
dataIcon="inline-end"
hasIcon={hasIcon}
icon={icon}
showSpinner={loading}
/>
) : null}
</motion.span>
);
});
const rippleTransition = {
duration: 0.65,
ease: [0.22, 1, 0.36, 1] as const,
opacity: {
duration: 0.65,
ease: [0.4, 0, 0.2, 1] as const,
times: [0, 0.2, 1] as const,
},
} as const;
const RippleLayer = memo(function RippleLayer({
onRippleComplete,
ripples,
}: {
ripples: Ripple[];
onRippleComplete: (id: string) => void;
}) {
return (
<span aria-hidden className="pointer-events-none absolute inset-0 z-[1]">
{ripples.map((ripple) => (
<motion.span
animate={{ opacity: [0.36, 0.2, 0], scale: 1 }}
className="pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--button-ripple)]"
initial={{ opacity: 0.36, scale: 0 }}
key={ripple.id}
onAnimationComplete={() => onRippleComplete(ripple.id)}
style={{
height: ripple.size,
left: ripple.x,
top: ripple.y,
width: ripple.size,
}}
transition={rippleTransition}
/>
))}
</span>
);
});
type ButtonInteractionElement = HTMLButtonElement | HTMLAnchorElement;
type ButtonInteractionHandlers = {
onBlur?: FocusEventHandler<ButtonInteractionElement>;
onKeyDown?: KeyboardEventHandler<ButtonInteractionElement>;
onKeyUp?: KeyboardEventHandler<ButtonInteractionElement>;
onPointerCancel?: PointerEventHandler<ButtonInteractionElement>;
onPointerDown?: PointerEventHandler<ButtonInteractionElement>;
onPointerLeave?: PointerEventHandler<ButtonInteractionElement>;
onPointerUp?: PointerEventHandler<ButtonInteractionElement>;
};
export type ButtonProps = Omit<
ButtonHTMLAttributesForMotion,
keyof ButtonInteractionHandlers
> &
ButtonInteractionHandlers &
VariantProps<typeof buttonVariants> & {
animateSize?: boolean;
children?: ReactNode;
disableRipple?: boolean;
href?: string;
icon?: ReactNode;
iconLabel?: string;
iconPosition?: "start" | "end";
loading?: boolean;
loadingIcon?: ReactNode;
} & Pick<
AnchorHTMLAttributes<HTMLAnchorElement>,
| "download"
| "hrefLang"
| "media"
| "ping"
| "referrerPolicy"
| "rel"
| "target"
>;
const ButtonInner = forwardRef<
HTMLButtonElement | HTMLAnchorElement,
ButtonProps
>(
(
{
animateSize = false,
"aria-label": ariaLabel,
children,
className,
disabled,
disableRipple = false,
href,
icon,
iconLabel,
iconPosition = "start",
linkUnderline,
loading = false,
loadingIcon,
onBlur,
onKeyDown,
onKeyUp,
onPointerCancel,
onPointerDown,
onPointerLeave,
onPointerUp,
rel,
size,
style,
target,
type = "button",
variant,
...props
},
ref
) => {
const isLocked = Boolean(disabled || loading);
const accessibleName = iconLabel ?? ariaLabel;
const reduceMotion = useReducedMotion();
useIconOnlyA11yWarning(size, children, iconLabel, ariaLabel, loading);
const buttonNodeRef = useRef<HTMLButtonElement | HTMLAnchorElement | null>(
null
);
const rippleIdRef = useRef(0);
const wasLoadingRef = useRef(loading);
const [horizontalInset, setHorizontalInset] = useState(0);
const [isPressed, setIsPressed] = useState(false);
const [ripples, setRipples] = useState<Ripple[]>([]);
const shouldAnimateSize = animateSize && style?.width == null;
const [contentRef, bounds] = useMeasure<HTMLSpanElement>(shouldAnimateSize);
const lastContentWidthRef = useRef(0);
const resolvedLinkUnderline =
variant === "link" ? (linkUnderline ?? "motion") : undefined;
const useLinkMotionUnderline = resolvedLinkUnderline === "motion";
const canAnimate = !isLocked && reduceMotion !== true;
const allowsRipple =
!(disableRipple || loading) &&
variant !== "link" &&
!isLocked &&
reduceMotion !== true;
useLayoutEffect(() => {
if (bounds.width > 0) {
lastContentWidthRef.current = bounds.width;
}
}, [bounds.width]);
const contentWidth =
bounds.width > 0 ? bounds.width : lastContentWidthRef.current;
const animatedWidth =
shouldAnimateSize && contentWidth > 0
? Math.ceil(contentWidth + horizontalInset)
: undefined;
const setButtonRef = useCallback(
(node: HTMLButtonElement | HTMLAnchorElement | null) => {
buttonNodeRef.current = node;
setRef(ref, node);
},
[ref]
);
const measureHorizontalInset = useCallback(
(measuredContentWidth: number) => {
if (!(measuredContentWidth > 0)) {
return;
}
const buttonNode = buttonNodeRef.current;
if (!(buttonNode && animateSize)) {
return;
}
const styles = window.getComputedStyle(buttonNode);
const next =
(Number.parseFloat(styles.borderLeftWidth) || 0) +
(Number.parseFloat(styles.borderRightWidth) || 0) +
(Number.parseFloat(styles.paddingLeft) || 0) +
(Number.parseFloat(styles.paddingRight) || 0);
setHorizontalInset((current) => (current === next ? current : next));
},
[animateSize]
);
useLayoutEffect(() => {
measureHorizontalInset(contentWidth);
}, [contentWidth, measureHorizontalInset]);
const handleRippleComplete = useCallback((id: string) => {
setRipples((current) => {
if (!current.some((item) => item.id === id)) {
return current;
}
return current.filter((item) => item.id !== id);
});
}, []);
useEffect(() => {
if (isLocked) {
setIsPressed(false);
}
}, [isLocked]);
useLayoutEffect(() => {
const wasLoading = wasLoadingRef.current;
if (loading || wasLoading) {
setRipples([]);
}
if (loading) {
setIsPressed(false);
}
wasLoadingRef.current = loading;
}, [loading]);
const handlePointerDown = (
e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>
) => {
onPointerDown?.(e);
if (e.defaultPrevented || isLocked) {
return;
}
setIsPressed(true);
if (!(allowsRipple && e.button === 0)) {
return;
}
if (e.pointerType === "mouse" && e.buttons !== 1) {
return;
}
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const rippleSize =
2 *
Math.max(
Math.hypot(x, y),
Math.hypot(rect.width - x, y),
Math.hypot(x, rect.height - y),
Math.hypot(rect.width - x, rect.height - y)
);
rippleIdRef.current += 1;
const id = `ripple-${rippleIdRef.current}`;
setRipples((current) => {
const next = [...current, { id, x, y, size: rippleSize }];
return next.length > MAX_RIPPLES
? next.slice(next.length - MAX_RIPPLES)
: next;
});
};
const handlePointerUp = (
e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>
) => {
onPointerUp?.(e);
setIsPressed(false);
};
const handlePointerLeave = (
e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>
) => {
onPointerLeave?.(e);
setIsPressed(false);
};
const handlePointerCancel = (
e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>
) => {
onPointerCancel?.(e);
setIsPressed(false);
};
const handleKeyDown = (e: KeyboardEvent<ButtonInteractionElement>) => {
onKeyDown?.(e);
if (
e.defaultPrevented ||
isLocked ||
e.repeat ||
(e.key !== " " && e.key !== "Enter")
) {
return;
}
setIsPressed(true);
};
const handleKeyUp = (e: KeyboardEvent<ButtonInteractionElement>) => {
onKeyUp?.(e);
if (e.key === " " || e.key === "Enter") {
setIsPressed(false);
}
};
const rootClassName = cn(
buttonVariants({
className,
linkUnderline: resolvedLinkUnderline,
size,
variant,
}),
isLocked && "pointer-events-none",
disabled && "opacity-50"
);
const buildButtonSurface = (labelContent: ReactNode) => (
<>
{allowsRipple ? (
<RippleLayer
onRippleComplete={handleRippleComplete}
ripples={ripples}
/>
) : null}
<ButtonLoadingContent
accessibleName={accessibleName}
contentRef={shouldAnimateSize ? contentRef : undefined}
icon={icon}
iconPosition={iconPosition}
linkUnderlineEnabled={resolvedLinkUnderline === "motion"}
loading={loading}
loadingIcon={loadingIcon}
reduceMotion={reduceMotion}
>
{labelContent}
</ButtonLoadingContent>
</>
);
const buttonSurface = buildButtonSurface(children);
const motionAnimate = useMemo(
() =>
getButtonAnimate(canAnimate, isPressed, animatedWidth, reduceMotion),
[animatedWidth, canAnimate, isPressed, reduceMotion]
);
const motionSurfaceProps = useMemo(
() =>
buildMotionSurfaceProps({
canAnimate,
isPressed,
motionAnimate,
reduceMotion,
useLinkMotionUnderline,
variant,
}),
[
canAnimate,
isPressed,
motionAnimate,
reduceMotion,
useLinkMotionUnderline,
variant,
]
);
const sharedRootProps = {
"aria-busy": loading || undefined,
"aria-label": accessibleName,
className: rootClassName,
"data-slot": "button" as const,
};
if (href?.trim()) {
const safeHref = href.trim();
const {
onClick,
type: _type,
...anchorProps
} = omitLinkMotionConflicts(props as AnchorHTMLAttributesForMotion);
const safeRel = resolveAnchorRel(target, rel);
return (
<MotionAnchor
{...motionSurfaceProps}
{...anchorProps}
{...sharedRootProps}
aria-disabled={isLocked || undefined}
href={safeHref}
onBlur={(e) => {
onBlur?.(e);
setIsPressed(false);
}}
onClick={(e) => {
if (isLocked) {
e.preventDefault();
}
onClick?.(e as React.MouseEvent<HTMLAnchorElement>);
}}
onKeyDown={(e) => {
onKeyDown?.(e);
if (
e.defaultPrevented ||
isLocked ||
e.repeat ||
e.key !== "Enter"
) {
return;
}
setIsPressed(true);
}}
onKeyUp={(e) => {
onKeyUp?.(e);
if (e.key === "Enter") {
setIsPressed(false);
}
}}
onPointerCancel={handlePointerCancel}
onPointerDown={handlePointerDown}
onPointerLeave={handlePointerLeave}
onPointerUp={handlePointerUp}
ref={setButtonRef}
rel={safeRel}
style={style}
tabIndex={isLocked ? -1 : undefined}
target={target}
>
{buttonSurface}
</MotionAnchor>
);
}
return (
<ButtonPrimitive
{...sharedRootProps}
disabled={isLocked}
onBlur={(e) => {
onBlur?.(e);
setIsPressed(false);
}}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onPointerCancel={handlePointerCancel}
onPointerDown={handlePointerDown}
onPointerLeave={handlePointerLeave}
onPointerUp={handlePointerUp}
render={(buttonProps) => {
const {
primitiveClassName,
primitiveRef,
primitiveStyle,
resolvedButtonProps,
} = resolvePrimitiveButtonProps(buttonProps);
return (
<motion.button
{...resolvedButtonProps}
{...motionSurfaceProps}
className={primitiveClassName}
ref={(node) => {
setButtonRef(node);
setRef(primitiveRef, node);
}}
style={primitiveStyle}
type={type}
>
{buttonSurface}
</motion.button>
);
}}
style={style}
type={type}
{...props}
/>
);
}
);
ButtonInner.displayName = "Button";
const Button = memo(ButtonInner);
export { Button, buttonVariants };
Base Button
Try the interaction, copy the React source files, or download the complete example.
About Base Button
Base Button is an animated React and Tailwind CSS button by Edwin Vakayil. Try the interaction, copy the React source, or download the example with creator credit.
- Effects and categories
- Hover effects, Animated
- Using this example
- Copy a TSX file or download the original source and standalone demo. Test keyboard focus, motion and contrast in your own project.
Usage, Tailwind and licensing questions
Read the source files
import-examples/base-button.tsx
import {Button} from "@/registry/b-button";export default function Demo(){return <div className="flex flex-wrap gap-3"><Button>Continue</Button><Button variant="outline">Outline</Button><Button loading>Saving</Button></div>}registry/b-button.tsx
"use client";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
type AnchorHTMLAttributes,
type ButtonHTMLAttributes,
cloneElement,
type FocusEventHandler,
forwardRef,
isValidElement,
type KeyboardEvent,
type KeyboardEventHandler,
memo,
type PointerEvent,
type PointerEventHandler,
type ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
const componentThemeClassName =
"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]";
const buttonThemeClassName =
"[--button-destructive:#dc2626] [--button-input:#e3e7ec] [--button-primary-foreground:#ffffff] [--button-ring:rgba(17,17,17,0.16)] [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] [--button-secondary:#eceff3] [--button-secondary-foreground:#111111] dark:[--button-destructive:#f87171] dark:[--button-input:#2b2a25] dark:[--button-primary-foreground:#111111] dark:[--button-ring:rgba(246,243,236,0.18)] dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_30%)] dark:[--button-secondary:#2a2a27] dark:[--button-secondary-foreground:#f6f3ec]";
const buttonCornerClassName =
"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]";
const buttonCornerXsClassName =
"rounded-[min(var(--radius-md),10px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[12px]";
const buttonCornerSmClassName =
"rounded-[min(var(--radius-md),12px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[14px]";
const buttonGroupCornerClassName =
"in-data-[slot=button-group]:rounded-lg in-data-[slot=button-group]:supports-[corner-shape:squircle]:corner-squircle in-data-[slot=button-group]:supports-[corner-shape:squircle]:rounded-[11px]";
const buttonVariants = cva(
cn(
componentThemeClassName,
buttonThemeClassName,
buttonCornerClassName,
"group/button relative isolate inline-flex shrink-0 cursor-pointer touch-manipulation select-none items-center justify-center overflow-hidden whitespace-nowrap border border-transparent bg-clip-border font-medium text-sm no-underline outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:border-[color:var(--button-ring)] focus-visible:ring-3 focus-visible:ring-[color:color-mix(in_oklch,var(--button-ring),transparent_50%)] active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-[color:var(--button-destructive)] aria-invalid:ring-3 aria-invalid:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:aria-invalid:border-[color:color-mix(in_oklch,var(--button-destructive),transparent_50%)] dark:aria-invalid:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
),
{
variants: {
variant: {
default:
"visited:!text-primary-foreground hover:!text-primary-foreground focus-visible:!text-primary-foreground active:!text-primary-foreground bg-primary text-primary-foreground [--button-ripple:color-mix(in_oklch,var(--primary-foreground),transparent_45%)] hover:bg-primary dark:[--button-ripple:color-mix(in_oklch,#111111,transparent_10%)] [&_svg]:text-primary-foreground",
outline:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground aria-expanded:!text-foreground border-border bg-background text-foreground [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] hover:bg-accent/60 aria-expanded:bg-muted dark:border-[color:var(--button-input)] dark:bg-[color-mix(in_oklch,var(--button-input),transparent_70%)] dark:hover:bg-[color-mix(in_oklch,var(--button-input),transparent_50%)] dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_28%)] [&_svg]:text-foreground",
secondary:
"visited:!text-[color:var(--button-secondary-foreground)] hover:!text-[color:var(--button-secondary-foreground)] focus-visible:!text-[color:var(--button-secondary-foreground)] active:!text-[color:var(--button-secondary-foreground)] aria-expanded:!text-[color:var(--button-secondary-foreground)] bg-[color:var(--button-secondary)] text-[color:var(--button-secondary-foreground)] [--button-ripple:color-mix(in_oklch,var(--button-secondary-foreground),transparent_52%)] hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_7%)] aria-expanded:bg-[color:var(--button-secondary)] dark:hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_11%)] dark:[--button-ripple:color-mix(in_oklch,var(--button-secondary-foreground),transparent_20%)] [&_svg]:text-[color:var(--button-secondary-foreground)]",
ghost:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground aria-expanded:!text-foreground text-foreground [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] hover:bg-accent/60 aria-expanded:bg-muted dark:hover:bg-muted/50 dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_28%)] [&_svg]:text-foreground",
destructive:
"visited:!text-[color:var(--button-destructive)] hover:!text-[color:var(--button-destructive)] focus-visible:!text-[color:var(--button-destructive)] active:!text-[color:var(--button-destructive)] bg-[color-mix(in_oklch,var(--button-destructive),transparent_90%)] text-[color:var(--button-destructive)] [--button-ripple:color-mix(in_oklch,var(--button-destructive),transparent_40%)] hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] focus-visible:border-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] dark:hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_70%)] dark:[--button-ripple:color-mix(in_oklch,var(--button-destructive),transparent_20%)] [&_svg]:text-[color:var(--button-destructive)]",
link: "",
},
linkUnderline: {
motion: "",
static: "",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: cn(
"h-6 gap-1 px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
buttonGroupCornerClassName,
buttonCornerXsClassName
),
sm: cn(
"h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
buttonGroupCornerClassName,
buttonCornerSmClassName
),
lg: "h-10 gap-1.5 px-4 text-base leading-5 has-data-[icon=inline-end]:pr-3.5 has-data-[icon=inline-start]:pl-3.5",
icon: "size-8",
"icon-xs": cn(
"size-6 [&_svg:not([class*='size-'])]:size-3",
buttonGroupCornerClassName,
buttonCornerXsClassName
),
"icon-sm": cn(
"size-7",
buttonGroupCornerClassName,
buttonCornerSmClassName
),
"icon-lg": "size-9",
},
},
compoundVariants: [
{
variant: "link",
linkUnderline: "motion",
class:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground px-0 text-foreground shadow-none active:translate-y-0 [&_[data-link-label]]:font-medium [&_[data-link-label]]:text-[1em]",
},
{
variant: "link",
linkUnderline: "static",
class:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground px-0 text-foreground underline-offset-4 hover:underline [&_[data-link-label]]:font-medium [&_[data-link-label]]:text-[1em]",
},
{
variant: "link",
size: "default",
class: "h-8 px-0 text-sm leading-5",
},
{
variant: "link",
size: "lg",
class: "h-10 px-0 text-base leading-5",
},
{
variant: "link",
size: "sm",
class: "h-7 px-0 text-[0.8rem]",
},
{
variant: "link",
size: "xs",
class: "h-6 px-0 text-xs",
},
],
defaultVariants: {
variant: "default",
size: "default",
linkUnderline: "motion",
},
}
);
const linkUnderlineFillTransition = {
type: "spring" as const,
stiffness: 420,
damping: 36,
mass: 0.65,
};
const MAX_RIPPLES = 3;
const REL_TOKEN_SPLIT = /\s+/;
const LinkUnderlineLabel = memo(function LinkUnderlineLabel({
children,
enabled,
}: {
children: ReactNode;
enabled: boolean;
}) {
if (!enabled) {
return <>{children}</>;
}
return (
<span className="relative inline-block pb-px font-medium text-[1em] leading-[inherit]">
<span className="relative z-10" data-link-label>
{children}
</span>
<span
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-px bg-muted-foreground/50"
/>
<motion.span
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-px origin-left bg-foreground"
transition={linkUnderlineFillTransition}
variants={{
rest: { scaleX: 0 },
hover: { scaleX: 1 },
}}
/>
</span>
);
});
const buttonContentClassName =
"relative z-10 inline-flex items-center justify-center [gap:inherit] whitespace-nowrap text-inherit";
type Ripple = { id: string; x: number; y: number; size: number };
type AnchorHTMLAttributesForMotion = Omit<
AnchorHTMLAttributes<HTMLAnchorElement>,
| "onAnimationEnd"
| "onAnimationIteration"
| "onAnimationStart"
| "onDrag"
| "onDragEnd"
| "onDragEnter"
| "onDragExit"
| "onDragLeave"
| "onDragOver"
| "onDragStart"
| "onDrop"
>;
type ButtonHTMLAttributesForMotion = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
| "onAnimationEnd"
| "onAnimationIteration"
| "onAnimationStart"
| "onDrag"
| "onDragEnd"
| "onDragEnter"
| "onDragExit"
| "onDragLeave"
| "onDragOver"
| "onDragStart"
| "onDrop"
>;
type ButtonRenderProps = React.HTMLAttributes<HTMLButtonElement> & {
className?: string;
ref?: React.Ref<HTMLButtonElement>;
style?: React.CSSProperties;
};
function setRef<T>(ref: React.Ref<T> | undefined, value: T) {
if (typeof ref === "function") {
ref(value);
return;
}
if (ref) {
(ref as React.MutableRefObject<T>).current = value;
}
}
function useMeasure<T extends HTMLElement = HTMLElement>(
enabled: boolean
): [(node: T | null) => void, { width: number; height: number }] {
const [element, setElement] = useState<T | null>(null);
const [bounds, setBounds] = useState({ width: 0, height: 0 });
const ref = useCallback((node: T | null) => {
setElement(node);
}, []);
useEffect(() => {
if (!(enabled && element && typeof ResizeObserver !== "undefined")) {
return;
}
const observer = new ResizeObserver(([entry]) => {
setBounds((current) => {
const next = {
width: entry.contentRect.width,
height: entry.contentRect.height,
};
if (current.width === next.width && current.height === next.height) {
return current;
}
return next;
});
});
observer.observe(element);
return () => observer.disconnect();
}, [element, enabled]);
return [ref, enabled ? bounds : { width: 0, height: 0 }];
}
const MotionAnchor = motion.create("a");
const buttonTransition = {
scale: {
type: "spring",
stiffness: 640,
damping: 38,
mass: 0.85,
},
width: {
type: "spring",
stiffness: 420,
damping: 34,
mass: 0.9,
},
opacity: {
type: "spring",
stiffness: 380,
damping: 32,
mass: 0.8,
},
y: { type: "spring", stiffness: 340, damping: 28, mass: 0.8 },
} as const;
const loadingIconTransition = {
type: "spring" as const,
stiffness: 400,
damping: 30,
mass: 0.76,
};
const loadingLabelExitTransition = {
type: "spring" as const,
stiffness: 420,
damping: 38,
mass: 0.84,
};
const loadingLabelEnterTransition = {
type: "spring" as const,
stiffness: 280,
damping: 30,
mass: 0.98,
};
const loadingLayoutTransition = {
type: "spring" as const,
stiffness: 360,
damping: 36,
mass: 0.94,
};
const loadingLabelVariants = {
animate: {
filter: "blur(0px)",
opacity: 1,
scale: 1,
transition: loadingLabelEnterTransition,
y: 0,
},
exit: {
filter: "blur(3px)",
opacity: 0,
scale: 0.96,
transition: loadingLabelExitTransition,
y: -4,
},
initial: {
filter: "blur(3px)",
opacity: 0,
scale: 0.96,
y: 4,
},
} as const;
function getLabelMotionKey(children: ReactNode, loading: boolean) {
if (typeof children === "string" || typeof children === "number") {
return `${loading ? "busy" : "idle"}:${children}`;
}
return loading ? "busy" : "idle";
}
function withLoadingSpin(node: ReactNode, spinning: boolean) {
if (!(spinning && isValidElement<{ className?: string }>(node))) {
return node;
}
return cloneElement(node, {
className: cn(node.props.className, "animate-spin"),
});
}
function getButtonAnimate(
canAnimate: boolean,
isPressed: boolean,
animatedWidth: number | undefined,
reduceMotion: boolean | null
) {
const shouldReduceMotion = reduceMotion === true;
const pressScale = canAnimate && isPressed && !shouldReduceMotion ? 0.96 : 1;
if (animatedWidth) {
return {
scale: pressScale,
width: animatedWidth,
};
}
return {
scale: pressScale,
};
}
function omitLinkMotionConflicts<T extends Record<string, unknown>>(props: T) {
const {
onAnimationEnd: _onAnimationEnd,
onAnimationIteration: _onAnimationIteration,
onAnimationStart: _onAnimationStart,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
onDragEnter: _onDragEnter,
onDragExit: _onDragExit,
onDragLeave: _onDragLeave,
onDragOver: _onDragOver,
onDragStart: _onDragStart,
onDrop: _onDrop,
...rest
} = props;
return rest;
}
function resolvePrimitiveButtonProps(buttonProps: ButtonRenderProps) {
const {
children: _buttonChildren,
className: primitiveClassName,
onAnimationEnd: _onAnimationEnd,
onAnimationIteration: _onAnimationIteration,
onAnimationStart: _onAnimationStart,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
onDragEnter: _onDragEnter,
onDragExit: _onDragExit,
onDragLeave: _onDragLeave,
onDragOver: _onDragOver,
onDragStart: _onDragStart,
onDrop: _onDrop,
ref: primitiveRef,
style: primitiveStyle,
...resolvedButtonProps
} = buttonProps;
return {
primitiveClassName,
primitiveRef,
primitiveStyle,
resolvedButtonProps,
};
}
function resolveAnchorRel(target?: string, rel?: string) {
if (!target?.includes("_blank")) {
return rel;
}
const tokens = new Set((rel ?? "").split(REL_TOKEN_SPLIT).filter(Boolean));
tokens.add("noopener");
tokens.add("noreferrer");
return [...tokens].join(" ");
}
function isIconOnlySize(
size:
| "default"
| "xs"
| "sm"
| "lg"
| "icon"
| "icon-xs"
| "icon-sm"
| "icon-lg"
| null
| undefined
) {
return (
size === "icon" ||
size === "icon-xs" ||
size === "icon-sm" ||
size === "icon-lg"
);
}
function useIconOnlyA11yWarning(
size:
| "default"
| "xs"
| "sm"
| "lg"
| "icon"
| "icon-xs"
| "icon-sm"
| "icon-lg"
| null
| undefined,
children: ReactNode,
iconLabel: string | undefined,
ariaLabel: string | undefined,
loading: boolean
) {
useEffect(() => {
if (process.env.NODE_ENV !== "development") {
return;
}
const hasVisibleLabel =
children != null && children !== false && children !== "";
const needsAccessibleName =
isIconOnlySize(size) || (loading && !hasVisibleLabel);
if (!needsAccessibleName || iconLabel || ariaLabel) {
return;
}
console.warn(
"[Button] Icon-only and loading-only buttons need `iconLabel` or `aria-label` for accessibility."
);
}, [ariaLabel, children, iconLabel, loading, size]);
}
function getIconIdleAnimate(showSpinner: boolean, noMotion: boolean) {
if (noMotion) {
return { opacity: showSpinner ? 0 : 1 };
}
return {
filter: showSpinner ? "blur(3px)" : "blur(0px)",
opacity: showSpinner ? 0 : 1,
rotate: showSpinner ? -56 : 0,
scale: showSpinner ? 0.82 : 1,
};
}
function getIconSpinnerAnimate(showSpinner: boolean, noMotion: boolean) {
if (noMotion) {
return { opacity: showSpinner ? 1 : 0 };
}
return {
filter: showSpinner ? "blur(0px)" : "blur(3px)",
opacity: showSpinner ? 1 : 0,
rotate: showSpinner ? 0 : 56,
scale: showSpinner ? 1 : 0.82,
};
}
function buildMotionSurfaceProps({
canAnimate,
isPressed,
motionAnimate,
reduceMotion,
useLinkMotionUnderline,
variant,
}: {
canAnimate: boolean;
isPressed: boolean;
motionAnimate: ReturnType<typeof getButtonAnimate>;
reduceMotion: boolean | null;
useLinkMotionUnderline: boolean;
variant: VariantProps<typeof buttonVariants>["variant"];
}) {
const shouldReduceMotion = reduceMotion === true;
const surfaceTransition = shouldReduceMotion
? { duration: 0 }
: buttonTransition;
const shared = {
animate: motionAnimate,
"data-pressed": isPressed ? "true" : "false",
transition: surfaceTransition,
};
if (useLinkMotionUnderline) {
return {
...shared,
initial: shouldReduceMotion ? (false as const) : ("rest" as const),
variants: shouldReduceMotion ? undefined : { rest: {}, hover: {} },
whileHover: shouldReduceMotion ? undefined : ("hover" as const),
};
}
const hoverLift =
canAnimate && variant !== "link" && !shouldReduceMotion
? { y: -1 }
: undefined;
return {
...shared,
initial: false as const,
whileHover: hoverLift,
};
}
const ButtonIconSlot = memo(function ButtonIconSlot({
accessibleName,
dataIcon,
hasIcon,
hasLabel,
icon,
iconTransition,
layoutTransition,
noMotion,
showSpinner,
spinner,
}: {
accessibleName?: string;
dataIcon: "inline-start" | "inline-end";
hasIcon: boolean;
hasLabel: boolean;
icon?: ReactNode;
iconTransition: { duration: number } | typeof loadingIconTransition;
layoutTransition: { duration: number } | typeof loadingLayoutTransition;
noMotion: boolean;
showSpinner: boolean;
spinner: ReactNode;
}) {
return (
<motion.span
aria-hidden={hasLabel || accessibleName ? true : undefined}
className="relative inline-flex size-[1em] shrink-0 items-center justify-center"
data-icon={dataIcon}
layout={noMotion ? false : "position"}
transition={layoutTransition}
>
{hasIcon ? (
<motion.span
animate={getIconIdleAnimate(showSpinner, noMotion)}
className="absolute inset-0 inline-flex items-center justify-center"
initial={false}
transition={iconTransition}
>
{icon}
</motion.span>
) : null}
<motion.span
animate={getIconSpinnerAnimate(showSpinner, noMotion)}
className="absolute inset-0 inline-flex items-center justify-center"
initial={false}
transition={iconTransition}
>
{withLoadingSpin(spinner, showSpinner && !noMotion)}
</motion.span>
</motion.span>
);
});
const ButtonLoadingLabel = memo(function ButtonLoadingLabel({
children,
layoutTransition,
linkUnderlineEnabled,
loading,
noMotion,
}: {
children: ReactNode;
layoutTransition: { duration: number } | typeof loadingLayoutTransition;
linkUnderlineEnabled: boolean;
loading: boolean;
noMotion: boolean;
}) {
return (
<motion.span
className="relative inline-grid min-w-0 items-center leading-[inherit] [grid-template-columns:max-content] [grid-template-rows:minmax(0,1.25em)]"
layout="position"
transition={layoutTransition}
>
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={noMotion ? { opacity: 1 } : "animate"}
className="col-start-1 row-start-1 inline-flex min-w-0 items-center whitespace-nowrap"
exit={noMotion ? { opacity: 0 } : "exit"}
initial={noMotion ? false : "initial"}
key={getLabelMotionKey(children, loading)}
layout="position"
transition={{ layout: layoutTransition }}
variants={noMotion ? undefined : loadingLabelVariants}
>
<LinkUnderlineLabel enabled={linkUnderlineEnabled}>
{children}
</LinkUnderlineLabel>
</motion.span>
</AnimatePresence>
</motion.span>
);
});
const ButtonSpinnerPresence = memo(function ButtonSpinnerPresence({
iconSlot,
layoutTransition,
noMotion,
visible,
}: {
iconSlot: ReactNode;
layoutTransition: { duration: number } | typeof loadingLayoutTransition;
noMotion: boolean;
visible: boolean;
}) {
return (
<AnimatePresence initial={false} mode="popLayout">
{visible ? (
<motion.span
animate={noMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}
className="inline-flex shrink-0"
exit={
noMotion
? { opacity: 0 }
: {
filter: "blur(2px)",
opacity: 0,
scale: 0.86,
transition: loadingLabelExitTransition,
}
}
initial={noMotion ? false : { opacity: 0, scale: 0.86 }}
key="loading-spinner"
layout="position"
transition={layoutTransition}
>
{iconSlot}
</motion.span>
) : null}
</AnimatePresence>
);
});
const defaultLoadingIcon = <Loader2 aria-hidden strokeWidth={2.25} />;
const ButtonLoadingContent = memo(function ButtonLoadingContent({
accessibleName,
children,
contentRef,
icon,
iconPosition,
linkUnderlineEnabled,
loading,
loadingIcon,
reduceMotion,
}: {
accessibleName?: string;
children: ReactNode;
contentRef?: React.Ref<HTMLSpanElement>;
icon?: ReactNode;
iconPosition: "start" | "end";
linkUnderlineEnabled: boolean;
loading: boolean;
loadingIcon?: ReactNode;
reduceMotion: boolean | null;
}) {
const hasIcon = Boolean(icon);
const hasLabel = children != null && children !== false && children !== "";
const noMotion = reduceMotion === true;
const iconTransition = noMotion ? { duration: 0 } : loadingIconTransition;
const layoutTransition = noMotion ? { duration: 0 } : loadingLayoutTransition;
const spinner = loadingIcon ?? defaultLoadingIcon;
const iconAtStart = loading || (hasIcon && iconPosition === "start");
const iconAtEnd = hasIcon && !loading && iconPosition === "end";
const spinnerOnlyAtStart = loading && !hasIcon;
const iconSlotProps = {
accessibleName,
hasLabel,
iconTransition,
layoutTransition,
noMotion,
spinner,
};
return (
<motion.span
animate={loading && !noMotion ? { opacity: 0.96 } : { opacity: 1 }}
className={buttonContentClassName}
initial={false}
layout={!noMotion}
ref={contentRef}
transition={layoutTransition}
>
<ButtonSpinnerPresence
iconSlot={
<ButtonIconSlot
{...iconSlotProps}
dataIcon="inline-start"
hasIcon={hasIcon}
icon={icon}
showSpinner
/>
}
layoutTransition={layoutTransition}
noMotion={noMotion}
visible={spinnerOnlyAtStart}
/>
{hasIcon && iconAtStart ? (
<ButtonIconSlot
{...iconSlotProps}
dataIcon="inline-start"
hasIcon={hasIcon}
icon={icon}
showSpinner={loading}
/>
) : null}
{hasLabel ? (
<ButtonLoadingLabel
layoutTransition={layoutTransition}
linkUnderlineEnabled={linkUnderlineEnabled}
loading={loading}
noMotion={noMotion}
>
{children}
</ButtonLoadingLabel>
) : null}
{hasIcon && iconAtEnd ? (
<ButtonIconSlot
{...iconSlotProps}
dataIcon="inline-end"
hasIcon={hasIcon}
icon={icon}
showSpinner={loading}
/>
) : null}
</motion.span>
);
});
const rippleTransition = {
duration: 0.65,
ease: [0.22, 1, 0.36, 1] as const,
opacity: {
duration: 0.65,
ease: [0.4, 0, 0.2, 1] as const,
times: [0, 0.2, 1] as const,
},
} as const;
const RippleLayer = memo(function RippleLayer({
onRippleComplete,
ripples,
}: {
ripples: Ripple[];
onRippleComplete: (id: string) => void;
}) {
return (
<span aria-hidden className="pointer-events-none absolute inset-0 z-[1]">
{ripples.map((ripple) => (
<motion.span
animate={{ opacity: [0.36, 0.2, 0], scale: 1 }}
className="pointer-events-none absolute -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--button-ripple)]"
initial={{ opacity: 0.36, scale: 0 }}
key={ripple.id}
onAnimationComplete={() => onRippleComplete(ripple.id)}
style={{
height: ripple.size,
left: ripple.x,
top: ripple.y,
width: ripple.size,
}}
transition={rippleTransition}
/>
))}
</span>
);
});
type ButtonInteractionElement = HTMLButtonElement | HTMLAnchorElement;
type ButtonInteractionHandlers = {
onBlur?: FocusEventHandler<ButtonInteractionElement>;
onKeyDown?: KeyboardEventHandler<ButtonInteractionElement>;
onKeyUp?: KeyboardEventHandler<ButtonInteractionElement>;
onPointerCancel?: PointerEventHandler<ButtonInteractionElement>;
onPointerDown?: PointerEventHandler<ButtonInteractionElement>;
onPointerLeave?: PointerEventHandler<ButtonInteractionElement>;
onPointerUp?: PointerEventHandler<ButtonInteractionElement>;
};
export type ButtonProps = Omit<
ButtonHTMLAttributesForMotion,
keyof ButtonInteractionHandlers
> &
ButtonInteractionHandlers &
VariantProps<typeof buttonVariants> & {
animateSize?: boolean;
children?: ReactNode;
disableRipple?: boolean;
href?: string;
icon?: ReactNode;
iconLabel?: string;
iconPosition?: "start" | "end";
loading?: boolean;
loadingIcon?: ReactNode;
} & Pick<
AnchorHTMLAttributes<HTMLAnchorElement>,
| "download"
| "hrefLang"
| "media"
| "ping"
| "referrerPolicy"
| "rel"
| "target"
>;
const ButtonInner = forwardRef<
HTMLButtonElement | HTMLAnchorElement,
ButtonProps
>(
(
{
animateSize = false,
"aria-label": ariaLabel,
children,
className,
disabled,
disableRipple = false,
href,
icon,
iconLabel,
iconPosition = "start",
linkUnderline,
loading = false,
loadingIcon,
onBlur,
onKeyDown,
onKeyUp,
onPointerCancel,
onPointerDown,
onPointerLeave,
onPointerUp,
rel,
size,
style,
target,
type = "button",
variant,
...props
},
ref
) => {
const isLocked = Boolean(disabled || loading);
const accessibleName = iconLabel ?? ariaLabel;
const reduceMotion = useReducedMotion();
useIconOnlyA11yWarning(size, children, iconLabel, ariaLabel, loading);
const buttonNodeRef = useRef<HTMLButtonElement | HTMLAnchorElement | null>(
null
);
const rippleIdRef = useRef(0);
const wasLoadingRef = useRef(loading);
const [horizontalInset, setHorizontalInset] = useState(0);
const [isPressed, setIsPressed] = useState(false);
const [ripples, setRipples] = useState<Ripple[]>([]);
const shouldAnimateSize = animateSize && style?.width == null;
const [contentRef, bounds] = useMeasure<HTMLSpanElement>(shouldAnimateSize);
const lastContentWidthRef = useRef(0);
const resolvedLinkUnderline =
variant === "link" ? (linkUnderline ?? "motion") : undefined;
const useLinkMotionUnderline = resolvedLinkUnderline === "motion";
const canAnimate = !isLocked && reduceMotion !== true;
const allowsRipple =
!(disableRipple || loading) &&
variant !== "link" &&
!isLocked &&
reduceMotion !== true;
useLayoutEffect(() => {
if (bounds.width > 0) {
lastContentWidthRef.current = bounds.width;
}
}, [bounds.width]);
const contentWidth =
bounds.width > 0 ? bounds.width : lastContentWidthRef.current;
const animatedWidth =
shouldAnimateSize && contentWidth > 0
? Math.ceil(contentWidth + horizontalInset)
: undefined;
const setButtonRef = useCallback(
(node: HTMLButtonElement | HTMLAnchorElement | null) => {
buttonNodeRef.current = node;
setRef(ref, node);
},
[ref]
);
const measureHorizontalInset = useCallback(
(measuredContentWidth: number) => {
if (!(measuredContentWidth > 0)) {
return;
}
const buttonNode = buttonNodeRef.current;
if (!(buttonNode && animateSize)) {
return;
}
const styles = window.getComputedStyle(buttonNode);
const next =
(Number.parseFloat(styles.borderLeftWidth) || 0) +
(Number.parseFloat(styles.borderRightWidth) || 0) +
(Number.parseFloat(styles.paddingLeft) || 0) +
(Number.parseFloat(styles.paddingRight) || 0);
setHorizontalInset((current) => (current === next ? current : next));
},
[animateSize]
);
useLayoutEffect(() => {
measureHorizontalInset(contentWidth);
}, [contentWidth, measureHorizontalInset]);
const handleRippleComplete = useCallback((id: string) => {
setRipples((current) => {
if (!current.some((item) => item.id === id)) {
return current;
}
return current.filter((item) => item.id !== id);
});
}, []);
useEffect(() => {
if (isLocked) {
setIsPressed(false);
}
}, [isLocked]);
useLayoutEffect(() => {
const wasLoading = wasLoadingRef.current;
if (loading || wasLoading) {
setRipples([]);
}
if (loading) {
setIsPressed(false);
}
wasLoadingRef.current = loading;
}, [loading]);
const handlePointerDown = (
e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>
) => {
onPointerDown?.(e);
if (e.defaultPrevented || isLocked) {
return;
}
setIsPressed(true);
if (!(allowsRipple && e.button === 0)) {
return;
}
if (e.pointerType === "mouse" && e.buttons !== 1) {
return;
}
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const rippleSize =
2 *
Math.max(
Math.hypot(x, y),
Math.hypot(rect.width - x, y),
Math.hypot(x, rect.height - y),
Math.hypot(rect.width - x, rect.height - y)
);
rippleIdRef.current += 1;
const id = `ripple-${rippleIdRef.current}`;
setRipples((current) => {
const next = [...current, { id, x, y, size: rippleSize }];
return next.length > MAX_RIPPLES
? next.slice(next.length - MAX_RIPPLES)
: next;
});
};
const handlePointerUp = (
e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>
) => {
onPointerUp?.(e);
setIsPressed(false);
};
const handlePointerLeave = (
e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>
) => {
onPointerLeave?.(e);
setIsPressed(false);
};
const handlePointerCancel = (
e: PointerEvent<HTMLButtonElement | HTMLAnchorElement>
) => {
onPointerCancel?.(e);
setIsPressed(false);
};
const handleKeyDown = (e: KeyboardEvent<ButtonInteractionElement>) => {
onKeyDown?.(e);
if (
e.defaultPrevented ||
isLocked ||
e.repeat ||
(e.key !== " " && e.key !== "Enter")
) {
return;
}
setIsPressed(true);
};
const handleKeyUp = (e: KeyboardEvent<ButtonInteractionElement>) => {
onKeyUp?.(e);
if (e.key === " " || e.key === "Enter") {
setIsPressed(false);
}
};
const rootClassName = cn(
buttonVariants({
className,
linkUnderline: resolvedLinkUnderline,
size,
variant,
}),
isLocked && "pointer-events-none",
disabled && "opacity-50"
);
const buildButtonSurface = (labelContent: ReactNode) => (
<>
{allowsRipple ? (
<RippleLayer
onRippleComplete={handleRippleComplete}
ripples={ripples}
/>
) : null}
<ButtonLoadingContent
accessibleName={accessibleName}
contentRef={shouldAnimateSize ? contentRef : undefined}
icon={icon}
iconPosition={iconPosition}
linkUnderlineEnabled={resolvedLinkUnderline === "motion"}
loading={loading}
loadingIcon={loadingIcon}
reduceMotion={reduceMotion}
>
{labelContent}
</ButtonLoadingContent>
</>
);
const buttonSurface = buildButtonSurface(children);
const motionAnimate = useMemo(
() =>
getButtonAnimate(canAnimate, isPressed, animatedWidth, reduceMotion),
[animatedWidth, canAnimate, isPressed, reduceMotion]
);
const motionSurfaceProps = useMemo(
() =>
buildMotionSurfaceProps({
canAnimate,
isPressed,
motionAnimate,
reduceMotion,
useLinkMotionUnderline,
variant,
}),
[
canAnimate,
isPressed,
motionAnimate,
reduceMotion,
useLinkMotionUnderline,
variant,
]
);
const sharedRootProps = {
"aria-busy": loading || undefined,
"aria-label": accessibleName,
className: rootClassName,
"data-slot": "button" as const,
};
if (href?.trim()) {
const safeHref = href.trim();
const {
onClick,
type: _type,
...anchorProps
} = omitLinkMotionConflicts(props as AnchorHTMLAttributesForMotion);
const safeRel = resolveAnchorRel(target, rel);
return (
<MotionAnchor
{...motionSurfaceProps}
{...anchorProps}
{...sharedRootProps}
aria-disabled={isLocked || undefined}
href={safeHref}
onBlur={(e) => {
onBlur?.(e);
setIsPressed(false);
}}
onClick={(e) => {
if (isLocked) {
e.preventDefault();
}
onClick?.(e as React.MouseEvent<HTMLAnchorElement>);
}}
onKeyDown={(e) => {
onKeyDown?.(e);
if (
e.defaultPrevented ||
isLocked ||
e.repeat ||
e.key !== "Enter"
) {
return;
}
setIsPressed(true);
}}
onKeyUp={(e) => {
onKeyUp?.(e);
if (e.key === "Enter") {
setIsPressed(false);
}
}}
onPointerCancel={handlePointerCancel}
onPointerDown={handlePointerDown}
onPointerLeave={handlePointerLeave}
onPointerUp={handlePointerUp}
ref={setButtonRef}
rel={safeRel}
style={style}
tabIndex={isLocked ? -1 : undefined}
target={target}
>
{buttonSurface}
</MotionAnchor>
);
}
return (
<ButtonPrimitive
{...sharedRootProps}
disabled={isLocked}
onBlur={(e) => {
onBlur?.(e);
setIsPressed(false);
}}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onPointerCancel={handlePointerCancel}
onPointerDown={handlePointerDown}
onPointerLeave={handlePointerLeave}
onPointerUp={handlePointerUp}
render={(buttonProps) => {
const {
primitiveClassName,
primitiveRef,
primitiveStyle,
resolvedButtonProps,
} = resolvePrimitiveButtonProps(buttonProps);
return (
<motion.button
{...resolvedButtonProps}
{...motionSurfaceProps}
className={primitiveClassName}
ref={(node) => {
setButtonRef(node);
setRef(primitiveRef, node);
}}
style={primitiveStyle}
type={type}
>
{buttonSurface}
</motion.button>
);
}}
style={style}
type={type}
{...props}
/>
);
}
);
ButtonInner.displayName = "Button";
const Button = memo(ButtonInner);
export { Button, buttonVariants };
lib/utils.ts
import type { ClassValue } from "clsx";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
demo.tsx
export { default as default } from "@/import-examples/base-button";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
"Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--spacing: 0.25rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-base: 1rem;
--text-base--line-height: calc(1.5 / 1);
--font-weight-medium: 500;
--radius-md: calc(var(--radius) - 2px);
--animate-spin: spin 1s linear infinite;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-secondary: var(--secondary);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-destructive: var(--destructive);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring:where(:not(iframe)) {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
::-webkit-calendar-picker-indicator {
line-height: 1;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
@layer utilities {
.pointer-events-none {
pointer-events: none;
}
.visible {
visibility: visible;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.static {
position: static;
}
.inset-0 {
inset: 0px;
}
.inset-x-0 {
inset-inline: 0px;
}
.bottom-0 {
bottom: 0px;
}
.isolate {
isolation: isolate;
}
.z-10 {
z-index: 10;
}
.z-\[1\] {
z-index: 1;
}
.col-start-1 {
grid-column-start: 1;
}
.row-start-1 {
grid-row-start: 1;
}
.flex {
display: flex;
}
.inline-block {
display: inline-block;
}
.inline-flex {
display: inline-flex;
}
.inline-grid {
display: inline-grid;
}
.size-6 {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
.size-7 {
width: calc(var(--spacing) * 7);
height: calc(var(--spacing) * 7);
}
.size-8 {
width: calc(var(--spacing) * 8);
height: calc(var(--spacing) * 8);
}
.size-9 {
width: calc(var(--spacing) * 9);
height: calc(var(--spacing) * 9);
}
.size-\[1em\] {
width: 1em;
height: 1em;
}
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-7 {
height: calc(var(--spacing) * 7);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
.h-10 {
height: calc(var(--spacing) * 10);
}
.h-px {
height: 1px;
}
.min-w-0 {
min-width: 0px;
}
.shrink-0 {
flex-shrink: 0;
}
.origin-left {
transform-origin: 0;
}
.-translate-x-1\/2 {
--tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.animate-spin {
animation: var(--animate-spin);
}
.cursor-pointer {
cursor: pointer;
}
.touch-manipulation {
touch-action: manipulation;
}
.\[grid-template-columns\:max-content\] {
grid-template-columns: max-content;
}
.\[grid-template-rows\:minmax\(0\,1\.25em\)\] {
grid-template-rows: minmax(0,1.25em);
}
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.\[gap\:inherit\] {
gap: inherit;
}
.gap-1 {
gap: var(--spacing);
}
.gap-1\.5 {
gap: calc(var(--spacing) * 1.5);
}
.gap-3 {
gap: calc(var(--spacing) * 3);
}
.overflow-hidden {
overflow: hidden;
}
.rounded-\[min\(var\(--radius-md\)\,10px\)\] {
border-radius: min(var(--radius-md), 10px);
}
.rounded-\[min\(var\(--radius-md\)\,12px\)\] {
border-radius: min(var(--radius-md), 12px);
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
.rounded-lg {
border-radius: var(--radius);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-border {
border-color: var(--border);
}
.border-transparent {
border-color: transparent;
}
.bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_90\%\)\] {
background-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklch,var(--button-destructive),transparent 90%);
}
}
.bg-\[color\:var\(--button-secondary\)\] {
background-color: var(--button-secondary);
}
.bg-\[var\(--button-ripple\)\] {
background-color: var(--button-ripple);
}
.bg-background {
background-color: var(--background);
}
.bg-foreground {
background-color: var(--foreground);
}
.bg-muted-foreground\/50 {
background-color: var(--muted-foreground);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--muted-foreground) 50%, transparent);
}
}
.bg-primary {
background-color: var(--primary);
}
.bg-clip-border {
background-clip: border-box;
}
.px-0 {
padding-inline: 0px;
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-2\.5 {
padding-inline: calc(var(--spacing) * 2.5);
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.pb-px {
padding-bottom: 1px;
}
.text-base {
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.text-\[0\.8rem\] {
font-size: 0.8rem;
}
.text-\[1em\] {
font-size: 1em;
}
.leading-5 {
--tw-leading: calc(var(--spacing) * 5);
line-height: calc(var(--spacing) * 5);
}
.leading-\[inherit\] {
--tw-leading: inherit;
line-height: inherit;
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-\[color\:var\(--button-destructive\)\] {
color: var(--button-destructive);
}
.text-\[color\:var\(--button-secondary-foreground\)\] {
color: var(--button-secondary-foreground);
}
.text-foreground {
color: var(--foreground);
}
.text-inherit {
color: inherit;
}
.text-primary-foreground {
color: var(--primary-foreground);
}
.no-underline {
text-decoration-line: none;
}
.underline-offset-4 {
text-underline-offset: 4px;
}
.opacity-50 {
opacity: 50%;
}
.shadow-none {
--tw-shadow: 0 0 #0000;
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-\[background-color\,border-color\,color\,box-shadow\] {
transition-property: background-color,border-color,color,box-shadow;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.\[--button-destructive\:\#dc2626\] {
--button-destructive: #dc2626;
}
.\[--button-input\:\#e3e7ec\] {
--button-input: #e3e7ec;
}
.\[--button-primary-foreground\:\#ffffff\] {
--button-primary-foreground: #ffffff;
}
.\[--button-ring\:rgba\(17\,17\,17\,0\.16\)\] {
--button-ring: rgba(17,17,17,0.16);
}
.\[--button-ripple\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_40\%\)\] {
--button-ripple: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--button-destructive),transparent 40%);
}
}
.\[--button-ripple\:color-mix\(in_oklch\,var\(--button-secondary-foreground\)\,transparent_52\%\)\] {
--button-ripple: var(--button-secondary-foreground);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--button-secondary-foreground),transparent 52%);
}
}
.\[--button-ripple\:color-mix\(in_oklch\,var\(--foreground\)\,transparent_52\%\)\] {
--button-ripple: var(--foreground);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--foreground),transparent 52%);
}
}
.\[--button-ripple\:color-mix\(in_oklch\,var\(--primary-foreground\)\,transparent_45\%\)\] {
--button-ripple: var(--primary-foreground);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--primary-foreground),transparent 45%);
}
}
.\[--button-secondary-foreground\:\#111111\] {
--button-secondary-foreground: #111111;
}
.\[--button-secondary\:\#eceff3\] {
--button-secondary: #eceff3;
}
.\[--color-accent-foreground\:var\(--ic-accent-foreground\)\] {
--color-accent-foreground: var(--ic-accent-foreground);
}
.\[--color-accent\:var\(--ic-accent\)\] {
--color-accent: var(--ic-accent);
}
.\[--color-background\:var\(--ic-background\)\] {
--color-background: var(--ic-background);
}
.\[--color-border\:var\(--ic-border\)\] {
--color-border: var(--ic-border);
}
.\[--color-brand-soft\:var\(--ic-brand-soft\)\] {
--color-brand-soft: var(--ic-brand-soft);
}
.\[--color-brand\:var\(--ic-brand\)\] {
--color-brand: var(--ic-brand);
}
.\[--color-card-foreground\:var\(--ic-card-foreground\)\] {
--color-card-foreground: var(--ic-card-foreground);
}
.\[--color-card\:var\(--ic-card\)\] {
--color-card: var(--ic-card);
}
.\[--color-chart-1\:var\(--ic-chart-1\)\] {
--color-chart-1: var(--ic-chart-1);
}
.\[--color-chart-2\:var\(--ic-chart-2\)\] {
--color-chart-2: var(--ic-chart-2);
}
.\[--color-chart-3\:var\(--ic-chart-3\)\] {
--color-chart-3: var(--ic-chart-3);
}
.\[--color-chart-4\:var\(--ic-chart-4\)\] {
--color-chart-4: var(--ic-chart-4);
}
.\[--color-chart-5\:var\(--ic-chart-5\)\] {
--color-chart-5: var(--ic-chart-5);
}
.\[--color-destructive\:var\(--ic-destructive\)\] {
--color-destructive: var(--ic-destructive);
}
.\[--color-foreground\:var\(--ic-foreground\)\] {
--color-foreground: var(--ic-foreground);
}
.\[--color-input\:var\(--ic-input\)\] {
--color-input: var(--ic-input);
}
.\[--color-muted-foreground\:var\(--ic-muted-foreground\)\] {
--color-muted-foreground: var(--ic-muted-foreground);
}
.\[--color-muted\:var\(--ic-muted\)\] {
--color-muted: var(--ic-muted);
}
.\[--color-paper\:var\(--ic-paper\)\] {
--color-paper: var(--ic-paper);
}
.\[--color-popover-foreground\:var\(--ic-popover-foreground\)\] {
--color-popover-foreground: var(--ic-popover-foreground);
}
.\[--color-primary\:var\(--ic-primary\)\] {
--color-primary: var(--ic-primary);
}
.\[--color-ring\:var\(--ic-ring\)\] {
--color-ring: var(--ic-ring);
}
.\[--color-secondary\:var\(--ic-secondary\)\] {
--color-secondary: var(--ic-secondary);
}
.\[--ic-accent-foreground\:\#111111\] {
--ic-accent-foreground: #111111;
}
.\[--ic-accent\:\#f3f5f8\] {
--ic-accent: #f3f5f8;
}
.\[--ic-background\:\#ffffff\] {
--ic-background: #ffffff;
}
.\[--ic-border\:\#e3e7ec\] {
--ic-border: #e3e7ec;
}
.\[--ic-brand-soft\:\#bae6fd\] {
--ic-brand-soft: #bae6fd;
}
.\[--ic-brand\:\#0ea5e9\] {
--ic-brand: #0ea5e9;
}
.\[--ic-card-foreground\:\#111111\] {
--ic-card-foreground: #111111;
}
.\[--ic-card\:\#ffffff\] {
--ic-card: #ffffff;
}
.\[--ic-chart-1\:oklch\(0\.52_0\.19_254\)\] {
--ic-chart-1: oklch(0.52 0.19 254);
}
.\[--ic-chart-2\:oklch\(0\.74_0\.11_232\)\] {
--ic-chart-2: oklch(0.74 0.11 232);
}
.\[--ic-chart-3\:oklch\(0\.42_0\.16_262\)\] {
--ic-chart-3: oklch(0.42 0.16 262);
}
.\[--ic-chart-4\:oklch\(0\.84_0\.07_228\)\] {
--ic-chart-4: oklch(0.84 0.07 228);
}
.\[--ic-chart-5\:oklch\(0\.62_0\.14_240\)\] {
--ic-chart-5: oklch(0.62 0.14 240);
}
.\[--ic-destructive\:\#dc2626\] {
--ic-destructive: #dc2626;
}
.\[--ic-foreground\:\#111111\] {
--ic-foreground: #111111;
}
.\[--ic-input\:\#e3e7ec\] {
--ic-input: #e3e7ec;
}
.\[--ic-muted-foreground\:\#6d7480\] {
--ic-muted-foreground: #6d7480;
}
.\[--ic-muted\:\#f5f7fa\] {
--ic-muted: #f5f7fa;
}
.\[--ic-paper\:\#fcfcfd\] {
--ic-paper: #fcfcfd;
}
.\[--ic-popover-foreground\:\#111111\] {
--ic-popover-foreground: #111111;
}
.\[--ic-primary\:\#111111\] {
--ic-primary: #111111;
}
.\[--ic-ring\:rgba\(17\,17\,17\,0\.16\)\] {
--ic-ring: rgba(17,17,17,0.16);
}
.\[--ic-secondary\:\#646b75\] {
--ic-secondary: #646b75;
}
.\[--ic-shadow-soft\:0_18px_38px_-24px_rgba\(15\,23\,42\,0\.35\)\] {
--ic-shadow-soft: 0 18px 38px -24px rgba(15,23,42,0.35);
}
.\[--ic-surface-border\:\#e9edf2\] {
--ic-surface-border: #e9edf2;
}
.visited\:\!text-\[color\:var\(--button-destructive\)\]:visited {
color: var(--button-destructive) !important;
}
.visited\:\!text-\[color\:var\(--button-secondary-foreground\)\]:visited {
color: var(--button-secondary-foreground) !important;
}
.visited\:\!text-foreground:visited {
color: var(--foreground) !important;
}
.visited\:\!text-primary-foreground:visited {
color: var(--primary-foreground) !important;
}
@media (hover: hover) {
.hover\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\]:hover {
background-color: var(--button-destructive);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\]:hover {
background-color: color-mix(in oklch,var(--button-destructive),transparent 80%);
}
}
.hover\:bg-\[color-mix\(in_oklch\,var\(--button-secondary\)\,var\(--foreground\)_7\%\)\]:hover {
background-color: var(--button-secondary);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-\[color-mix\(in_oklch\,var\(--button-secondary\)\,var\(--foreground\)_7\%\)\]:hover {
background-color: color-mix(in oklch,var(--button-secondary),var(--foreground) 7%);
}
}
.hover\:bg-accent\/60:hover {
background-color: var(--accent);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-accent\/60:hover {
background-color: color-mix(in oklab, var(--accent) 60%, transparent);
}
}
.hover\:bg-primary:hover {
background-color: var(--primary);
}
.hover\:\!text-\[color\:var\(--button-destructive\)\]:hover {
color: var(--button-destructive) !important;
}
.hover\:\!text-\[color\:var\(--button-secondary-foreground\)\]:hover {
color: var(--button-secondary-foreground) !important;
}
.hover\:\!text-foreground:hover {
color: var(--foreground) !important;
}
.hover\:\!text-primary-foreground:hover {
color: var(--primary-foreground) !important;
}
.hover\:underline:hover {
text-decoration-line: underline;
}
}
.focus-visible\:border-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_60\%\)\]:focus-visible {
border-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklch,var(--button-destructive),transparent 60%);
}
}
.focus-visible\:border-\[color\:var\(--button-ring\)\]:focus-visible {
border-color: var(--button-ring);
}
.focus-visible\:\!text-\[color\:var\(--button-destructive\)\]:focus-visible {
color: var(--button-destructive) !important;
}
.focus-visible\:\!text-\[color\:var\(--button-secondary-foreground\)\]:focus-visible {
color: var(--button-secondary-foreground) !important;
}
.focus-visible\:\!text-foreground:focus-visible {
color: var(--foreground) !important;
}
.focus-visible\:\!text-primary-foreground:focus-visible {
color: var(--primary-foreground) !important;
}
.focus-visible\:ring-3:focus-visible {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.focus-visible\:ring-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\]:focus-visible {
--tw-ring-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklch,var(--button-destructive),transparent 80%);
}
}
.focus-visible\:ring-\[color\:color-mix\(in_oklch\,var\(--button-ring\)\,transparent_50\%\)\]:focus-visible {
--tw-ring-color: var(--button-ring);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklch,var(--button-ring),transparent 50%);
}
}
.active\:translate-y-0:active {
--tw-translate-y: 0px;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.active\:\!text-\[color\:var\(--button-destructive\)\]:active {
color: var(--button-destructive) !important;
}
.active\:\!text-\[color\:var\(--button-secondary-foreground\)\]:active {
color: var(--button-secondary-foreground) !important;
}
.active\:\!text-foreground:active {
color: var(--foreground) !important;
}
.active\:\!text-primary-foreground:active {
color: var(--primary-foreground) !important;
}
.active\:not-aria-\[haspopup\]\:translate-y-px:active:not([aria-haspopup]) {
--tw-translate-y: 1px;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.disabled\:pointer-events-none:disabled {
pointer-events: none;
}
.disabled\:opacity-50:disabled {
opacity: 50%;
}
:where([data-slot="button-group"]) .in-data-\[slot\=button-group\]\:rounded-lg {
border-radius: var(--radius);
}
.has-data-\[icon\=inline-end\]\:pr-1\.5:has([data-icon="inline-end"]) {
padding-right: calc(var(--spacing) * 1.5);
}
.has-data-\[icon\=inline-end\]\:pr-2:has([data-icon="inline-end"]) {
padding-right: calc(var(--spacing) * 2);
}
.has-data-\[icon\=inline-end\]\:pr-3\.5:has([data-icon="inline-end"]) {
padding-right: calc(var(--spacing) * 3.5);
}
.has-data-\[icon\=inline-start\]\:pl-1\.5:has([data-icon="inline-start"]) {
padding-left: calc(var(--spacing) * 1.5);
}
.has-data-\[icon\=inline-start\]\:pl-2:has([data-icon="inline-start"]) {
padding-left: calc(var(--spacing) * 2);
}
.has-data-\[icon\=inline-start\]\:pl-3\.5:has([data-icon="inline-start"]) {
padding-left: calc(var(--spacing) * 3.5);
}
.aria-expanded\:bg-\[color\:var\(--button-secondary\)\][aria-expanded="true"] {
background-color: var(--button-secondary);
}
.aria-expanded\:bg-muted[aria-expanded="true"] {
background-color: var(--muted);
}
.aria-expanded\:\!text-\[color\:var\(--button-secondary-foreground\)\][aria-expanded="true"] {
color: var(--button-secondary-foreground) !important;
}
.aria-expanded\:\!text-foreground[aria-expanded="true"] {
color: var(--foreground) !important;
}
.aria-invalid\:border-\[color\:var\(--button-destructive\)\][aria-invalid="true"] {
border-color: var(--button-destructive);
}
.aria-invalid\:ring-3[aria-invalid="true"] {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.aria-invalid\:ring-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\][aria-invalid="true"] {
--tw-ring-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklch,var(--button-destructive),transparent 80%);
}
}
@supports (corner-shape:squircle) {
.supports-\[corner-shape\:squircle\]\:rounded-\[11px\] {
border-radius: 11px;
}
.supports-\[corner-shape\:squircle\]\:rounded-\[12px\] {
border-radius: 12px;
}
.supports-\[corner-shape\:squircle\]\:rounded-\[14px\] {
border-radius: 14px;
}
:where([data-slot="button-group"]) .in-data-\[slot\=button-group\]\:supports-\[corner-shape\:squircle\]\:rounded-\[11px\] {
border-radius: 11px;
}
}
.dark\:border-\[color\:var\(--button-input\)\]:where(.dark, .dark *) {
border-color: var(--button-input);
}
.dark\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\]:where(.dark, .dark *) {
background-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklch,var(--button-destructive),transparent 80%);
}
}
.dark\:bg-\[color-mix\(in_oklch\,var\(--button-input\)\,transparent_70\%\)\]:where(.dark, .dark *) {
background-color: var(--button-input);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklch,var(--button-input),transparent 70%);
}
}
.dark\:\[--button-destructive\:\#f87171\]:where(.dark, .dark *) {
--button-destructive: #f87171;
}
.dark\:\[--button-input\:\#2b2a25\]:where(.dark, .dark *) {
--button-input: #2b2a25;
}
.dark\:\[--button-primary-foreground\:\#111111\]:where(.dark, .dark *) {
--button-primary-foreground: #111111;
}
.dark\:\[--button-ring\:rgba\(246\,243\,236\,0\.18\)\]:where(.dark, .dark *) {
--button-ring: rgba(246,243,236,0.18);
}
.dark\:\[--button-ripple\:color-mix\(in_oklch\,\#111111\,transparent_10\%\)\]:where(.dark, .dark *) {
--button-ripple: color-mix(in oklch,#111111,transparent 10%);
}
.dark\:\[--button-ripple\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_20\%\)\]:where(.dark, .dark *) {
--button-ripple: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--button-destructive),transparent 20%);
}
}
.dark\:\[--button-ripple\:color-mix\(in_oklch\,var\(--button-secondary-foreground\)\,transparent_20\%\)\]:where(.dark, .dark *) {
--button-ripple: var(--button-secondary-foreground);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--button-secondary-foreground),transparent 20%);
}
}
.dark\:\[--button-ripple\:color-mix\(in_oklch\,var\(--foreground\)\,transparent_28\%\)\]:where(.dark, .dark *) {
--button-ripple: var(--foreground);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--foreground),transparent 28%);
}
}
.dark\:\[--button-ripple\:color-mix\(in_oklch\,var\(--foreground\)\,transparent_30\%\)\]:where(.dark, .dark *) {
--button-ripple: var(--foreground);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--foreground),transparent 30%);
}
}
.dark\:\[--button-secondary-foreground\:\#f6f3ec\]:where(.dark, .dark *) {
--button-secondary-foreground: #f6f3ec;
}
.dark\:\[--button-secondary\:\#2a2a27\]:where(.dark, .dark *) {
--button-secondary: #2a2a27;
}
.dark\:\[--ic-accent-foreground\:\#f6f3ec\]:where(.dark, .dark *) {
--ic-accent-foreground: #f6f3ec;
}
.dark\:\[--ic-accent\:\#1a1a18\]:where(.dark, .dark *) {
--ic-accent: #1a1a18;
}
.dark\:\[--ic-background\:\#111111\]:where(.dark, .dark *) {
--ic-background: #111111;
}
.dark\:\[--ic-border\:\#2b2a25\]:where(.dark, .dark *) {
--ic-border: #2b2a25;
}
.dark\:\[--ic-brand-soft\:\#0c4a6e\]:where(.dark, .dark *) {
--ic-brand-soft: #0c4a6e;
}
.dark\:\[--ic-brand\:\#38bdf8\]:where(.dark, .dark *) {
--ic-brand: #38bdf8;
}
.dark\:\[--ic-card-foreground\:\#f6f3ec\]:where(.dark, .dark *) {
--ic-card-foreground: #f6f3ec;
}
.dark\:\[--ic-card\:\#111111\]:where(.dark, .dark *) {
--ic-card: #111111;
}
.dark\:\[--ic-chart-1\:oklch\(0\.68_0\.17_250\)\]:where(.dark, .dark *) {
--ic-chart-1: oklch(0.68 0.17 250);
}
.dark\:\[--ic-chart-2\:oklch\(0\.82_0\.09_225\)\]:where(.dark, .dark *) {
--ic-chart-2: oklch(0.82 0.09 225);
}
.dark\:\[--ic-chart-3\:oklch\(0\.58_0\.15_260\)\]:where(.dark, .dark *) {
--ic-chart-3: oklch(0.58 0.15 260);
}
.dark\:\[--ic-chart-4\:oklch\(0\.75_0\.12_235\)\]:where(.dark, .dark *) {
--ic-chart-4: oklch(0.75 0.12 235);
}
.dark\:\[--ic-chart-5\:oklch\(0\.88_0\.06_220\)\]:where(.dark, .dark *) {
--ic-chart-5: oklch(0.88 0.06 220);
}
.dark\:\[--ic-destructive\:\#f87171\]:where(.dark, .dark *) {
--ic-destructive: #f87171;
}
.dark\:\[--ic-foreground\:\#f6f3ec\]:where(.dark, .dark *) {
--ic-foreground: #f6f3ec;
}
.dark\:\[--ic-input\:\#2b2a25\]:where(.dark, .dark *) {
--ic-input: #2b2a25;
}
.dark\:\[--ic-muted-foreground\:\#9a958a\]:where(.dark, .dark *) {
--ic-muted-foreground: #9a958a;
}
.dark\:\[--ic-muted\:\#171716\]:where(.dark, .dark *) {
--ic-muted: #171716;
}
.dark\:\[--ic-paper\:\#171716\]:where(.dark, .dark *) {
--ic-paper: #171716;
}
.dark\:\[--ic-popover-foreground\:\#f6f3ec\]:where(.dark, .dark *) {
--ic-popover-foreground: #f6f3ec;
}
.dark\:\[--ic-primary\:\#f6f3ec\]:where(.dark, .dark *) {
--ic-primary: #f6f3ec;
}
.dark\:\[--ic-ring\:rgba\(246\,243\,236\,0\.18\)\]:where(.dark, .dark *) {
--ic-ring: rgba(246,243,236,0.18);
}
.dark\:\[--ic-secondary\:\#cbc6bb\]:where(.dark, .dark *) {
--ic-secondary: #cbc6bb;
}
.dark\:\[--ic-shadow-soft\:0_20px_44px_-28px_rgba\(0\,0\,0\,0\.6\)\]:where(.dark, .dark *) {
--ic-shadow-soft: 0 20px 44px -28px rgba(0,0,0,0.6);
}
.dark\:\[--ic-surface-border\:\#2a2a25\]:where(.dark, .dark *) {
--ic-surface-border: #2a2a25;
}
@media (hover: hover) {
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_70\%\)\]:where(.dark, .dark *):hover {
background-color: var(--button-destructive);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_70\%\)\]:where(.dark, .dark *):hover {
background-color: color-mix(in oklch,var(--button-destructive),transparent 70%);
}
}
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-input\)\,transparent_50\%\)\]:where(.dark, .dark *):hover {
background-color: var(--button-input);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-input\)\,transparent_50\%\)\]:where(.dark, .dark *):hover {
background-color: color-mix(in oklch,var(--button-input),transparent 50%);
}
}
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-secondary\)\,var\(--foreground\)_11\%\)\]:where(.dark, .dark *):hover {
background-color: var(--button-secondary);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-secondary\)\,var\(--foreground\)_11\%\)\]:where(.dark, .dark *):hover {
background-color: color-mix(in oklch,var(--button-secondary),var(--foreground) 11%);
}
}
.dark\:hover\:bg-muted\/50:where(.dark, .dark *):hover {
background-color: var(--muted);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-muted\/50:where(.dark, .dark *):hover {
background-color: color-mix(in oklab, var(--muted) 50%, transparent);
}
}
}
.dark\:focus-visible\:ring-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_60\%\)\]:where(.dark, .dark *):focus-visible {
--tw-ring-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklch,var(--button-destructive),transparent 60%);
}
}
.dark\:aria-invalid\:border-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_50\%\)\]:where(.dark, .dark *)[aria-invalid="true"] {
border-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklch,var(--button-destructive),transparent 50%);
}
}
.dark\:aria-invalid\:ring-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_60\%\)\]:where(.dark, .dark *)[aria-invalid="true"] {
--tw-ring-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklch,var(--button-destructive),transparent 60%);
}
}
.\[\&_\[data-link-label\]\]\:text-\[1em\] [data-link-label] {
font-size: 1em;
}
.\[\&_\[data-link-label\]\]\:font-medium [data-link-label] {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.\[\&_svg\]\:pointer-events-none svg {
pointer-events: none;
}
.\[\&_svg\]\:shrink-0 svg {
flex-shrink: 0;
}
.\[\&_svg\]\:text-\[color\:var\(--button-destructive\)\] svg {
color: var(--button-destructive);
}
.\[\&_svg\]\:text-\[color\:var\(--button-secondary-foreground\)\] svg {
color: var(--button-secondary-foreground);
}
.\[\&_svg\]\:text-foreground svg {
color: var(--foreground);
}
.\[\&_svg\]\:text-primary-foreground svg {
color: var(--primary-foreground);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*='size-']) {
width: calc(var(--spacing) * 3);
height: calc(var(--spacing) * 3);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5 svg:not([class*='size-']) {
width: calc(var(--spacing) * 3.5);
height: calc(var(--spacing) * 3.5);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*='size-']) {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
}
:root {
--background: #fff;
--foreground: #111;
--primary: #171717;
--primary-foreground: #fff;
--secondary: #eee;
--secondary-foreground: #171717;
--muted: #eee;
--muted-foreground: #666;
--accent: #eee;
--accent-foreground: #171717;
--border: #ddd;
--input: #ddd;
--ring: #777;
--destructive: #dc2626;
--destructive-foreground: #fff;
--popover: #fff;
--popover-foreground: #111;
--success: #15803d;
--success-foreground: #fff;
--radius: .625rem;
--hairline: 1px;
}
.dark {
--background: #090909;
--foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #171717;
--secondary: #262626;
--secondary-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a3a3a3;
--accent: #262626;
--accent-foreground: #fafafa;
--border: #333;
--input: #333;
--popover: #171717;
--popover-foreground: #fafafa;
}
:root {
--header-height-mobile: 3.5rem;
--header-height-desktop: 3.5rem;
--nav-stack-height-mobile: var(--header-height-mobile);
--nav-stack-height-desktop: var(--header-height-desktop);
--background: #ffffff;
--foreground: #111111;
--primary: #111111;
--primary-foreground: #ffffff;
--secondary: #646b75;
--secondary-foreground: #ffffff;
--surface-border: #e9edf2;
--border: #e3e7ec;
--card: #ffffff;
--card-foreground: #111111;
--muted: #f5f7fa;
--muted-foreground: #6d7480;
--accent: #f3f5f8;
--accent-foreground: #111111;
--input: #e3e7ec;
--ring: rgba(17, 17, 17, 0.16);
--destructive: #dc2626;
--accent-pro: #0ea5e9;
--sidebar-accent: #f5f7fa;
--sidebar-accent-foreground: #111111;
--separator-dash: rgb(227 231 236 / 1);
--selection-bg: #cfeeff;
--selection-fg: #0f172a;
--nav-menu-surface: #f4f4f5;
--nav-menu-item-hover: #e4e4e7;
--shadow-card: inset 0 1px 0 rgb(255 255 255 / 88%), inset 0 -1px 0 rgb(0 0 0 / 5%), 0 1px
1px rgb(0 0 0 / 3%), 0 8px 24px rgb(0 0 0 / 4%);
--radius: 0.4rem;
}
.dark {
--background: #111111;
--foreground: #f6f3ec;
--secondary-bg: #171716;
--secondary: #cbc6bb;
--secondary-foreground: #111111;
--primary: #f6f3ec;
--primary-foreground: #111111;
--surface-border: #2a2a25;
--border: #2b2a25;
--card: #111111;
--card-foreground: #f6f3ec;
--muted: #171716;
--muted-foreground: #9a958a;
--accent: #1a1a18;
--accent-foreground: #f6f3ec;
--input: #2b2a25;
--ring: rgba(246, 243, 236, 0.18);
--destructive: #f87171;
--accent-pro: #38bdf8;
--sidebar-accent: #262626;
--sidebar-accent-foreground: #fff;
--separator-dash: rgb(92 91 84 / 1);
--selection-bg: #cfeeff;
--selection-fg: #0f172a;
--nav-menu-surface: #1a1a1c;
--nav-menu-item-hover: #262628;
}
.dark .hero-brush-underline::after {
background: #16a34a;
}
@keyframes status-blink {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.28;
transform: scale(0.82);
}
}
@keyframes iconiq-accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes iconiq-accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
.dark main .text-neutral-500 {
color: rgb(163 163 163);
}
.dark main .text-neutral-600 {
color: rgb(212 212 212);
}
.dark main .text-neutral-700 {
color: rgb(230 230 230);
}
.dark main .text-neutral-800 {
color: rgb(245 245 245);
}
.dark main .text-neutral-900 {
color: rgb(255 255 255);
}
.dark main .text-neutral-400 {
color: rgb(163 163 163);
}
.dark main code {
color: rgb(212 212 212);
}
.dark main code.rounded, .dark main code.rounded-md, .dark main code[class*="bg-neutral-200"], .dark main code[class*="bg-neutral-50"] {
background-color: rgb(38 38 38);
color: rgb(230 230 230);
}
.dark code.rounded, .dark code.rounded-md, .dark code[class*="bg-neutral-200"] {
background-color: rgb(38 38 38);
color: rgb(230 230 230);
}
.dark code[class*="bg-amber-200"] {
background-color: rgb(69 69 40);
color: rgb(253 253 230);
}
.dark main a.text-neutral-700 {
color: rgb(230 230 230);
}
.dark main a:hover {
color: rgb(255 255 255);
}
.dark html[data-route-split-docs] [data-code-block] .shiki, .dark html[data-route-split-docs] [data-code-block] .shiki span {
color: var(--shiki-dark) !important;
}
@property --tw-translate-x {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-y {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-z {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-leading {
syntax: "*";
inherits: false;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
syntax: "*";
inherits: false;
}
@property --tw-ring-offset-width {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}
@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur {
syntax: "*";
inherits: false;
}
@property --tw-brightness {
syntax: "*";
inherits: false;
}
@property --tw-contrast {
syntax: "*";
inherits: false;
}
@property --tw-grayscale {
syntax: "*";
inherits: false;
}
@property --tw-hue-rotate {
syntax: "*";
inherits: false;
}
@property --tw-invert {
syntax: "*";
inherits: false;
}
@property --tw-opacity {
syntax: "*";
inherits: false;
}
@property --tw-saturate {
syntax: "*";
inherits: false;
}
@property --tw-sepia {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-drop-shadow-size {
syntax: "*";
inherits: false;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-border-style: solid;
--tw-leading: initial;
--tw-font-weight: initial;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-shadow-alpha: 100%;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-inset-shadow-alpha: 100%;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-outline-style: solid;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
}
}
}