"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
export type MorphStatus = "idle" | "loading" | "success" | "error";
export interface MorphStatusButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
/** Idle label. Falls back to `label`. */
children?: React.ReactNode;
/** Idle label used when no children are provided. */
label?: React.ReactNode;
/** Label shown while the action is in flight. */
loadingLabel?: React.ReactNode;
/** Label shown after the action resolves. */
successLabel?: React.ReactNode;
/** Label shown after the action rejects. */
errorLabel?: React.ReactNode;
/**
* Click handler. If it returns a promise, the button auto-transitions
* through loading and into success or error based on the outcome.
*/
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void | Promise<unknown>;
/**
* Controlled status. When provided, the button reflects this value and the
* promise-based auto-transitions are ignored.
*/
status?: MorphStatus;
/** Milliseconds to stay in success/error before resetting. Set to 0 to stay. */
resetAfter?: number;
}
const Spinner = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
className="size-4 shrink-0 animate-spin"
aria-hidden
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
const CheckIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4 shrink-0"
aria-hidden
>
<path d="M20 6 9 17l-5-5" />
</svg>
);
const XIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4 shrink-0"
aria-hidden
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
function textOf(node: React.ReactNode): string {
if (node === null || node === undefined || typeof node === "boolean") {
return "";
}
if (typeof node === "string" || typeof node === "number") {
return String(node);
}
if (Array.isArray(node)) return node.map(textOf).join("");
if (React.isValidElement<{ children?: React.ReactNode }>(node)) {
return textOf(node.props.children);
}
return "";
}
export const MorphStatusButton = React.forwardRef<
HTMLButtonElement,
MorphStatusButtonProps
>(
(
{
children,
label = "Save changes",
loadingLabel = "Working…",
successLabel = "Done",
errorLabel = "It broke. Your fault.",
onClick,
status: controlledStatus,
resetAfter = 1800,
className,
disabled,
...props
},
ref,
) => {
const reduceMotion = useReducedMotion();
const [internal, setInternal] = React.useState<MorphStatus>("idle");
const resetTimeoutRef = React.useRef<number | null>(null);
const isControlled = controlledStatus !== undefined;
const status = isControlled ? controlledStatus : internal;
const clearReset = () => {
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
resetTimeoutRef.current = null;
}
};
React.useEffect(() => () => clearReset(), []);
const scheduleReset = () => {
if (isControlled || resetAfter <= 0) return;
clearReset();
resetTimeoutRef.current = window.setTimeout(() => {
setInternal("idle");
}, resetAfter);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (status === "loading") return;
const result = onClick?.(e);
if (isControlled || !(result instanceof Promise)) return;
clearReset();
setInternal("loading");
result
.then(() => {
setInternal("success");
scheduleReset();
})
.catch(() => {
setInternal("error");
scheduleReset();
});
};
const idleLabel = children ?? label;
const currentLabel: React.ReactNode =
status === "loading"
? loadingLabel
: status === "success"
? successLabel
: status === "error"
? errorLabel
: idleLabel;
const icon =
status === "loading" ? (
<Spinner />
) : status === "success" ? (
<CheckIcon />
) : status === "error" ? (
<XIcon />
) : null;
// Widest possible label reserves width so the button never reflows.
const widest = [
idleLabel,
loadingLabel,
successLabel,
errorLabel,
].reduce<React.ReactNode>(
(widestNode, candidate) =>
textOf(candidate).length > textOf(widestNode).length
? candidate
: widestNode,
"",
);
return (
<button
ref={ref}
type="button"
onClick={handleClick}
disabled={disabled || status === "loading"}
aria-live="polite"
aria-busy={status === "loading"}
data-state={status}
className={cn(
"relative inline-flex h-9 cursor-pointer items-center justify-center overflow-hidden rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none",
// Graded dark surface matching MinimalButton: dark outer hairline, faint inner ring, top highlight, soft drop.
"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]",
"transition-[scale,filter,box-shadow,color] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110",
"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
// In flight, not disabled: stay solid with a dimmed label.
status === "loading" &&
"text-neutral-50/70 hover:brightness-100 disabled:opacity-100",
// Error keeps the dark face; a red label and inner ring carry the blame.
status === "error" &&
"text-red-300 shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(248_113_113/0.35),inset_0_1px_0_rgb(255_255_255/0.1),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(185_28_28/0.45)]",
className,
)}
{...props}
>
{/* Invisible widest label reserves a stable width. */}
<span aria-hidden className="invisible inline-flex items-center gap-2">
<span className="size-4" />
{widest}
</span>
<span className="absolute inset-0 flex items-center justify-center gap-2">
{icon ? (
<span className="relative inline-flex size-4 items-center justify-center">
<AnimatePresence initial={false} mode="wait">
<motion.span
key={status}
initial={reduceMotion ? false : { scale: 0.4, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={reduceMotion ? undefined : { scale: 0.4, opacity: 0 }}
transition={{ type: "spring", stiffness: 380, damping: 22 }}
className="inline-flex"
>
{icon}
</motion.span>
</AnimatePresence>
</span>
) : null}
<span className="relative inline-block">
<AnimatePresence initial={false} mode="wait">
<motion.span
key={status}
initial={reduceMotion ? false : { y: 8, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={reduceMotion ? undefined : { y: -8, opacity: 0 }}
transition={{ duration: 0.16 }}
className="inline-block whitespace-nowrap"
>
{currentLabel}
</motion.span>
</AnimatePresence>
</span>
</span>
</button>
);
},
);
MorphStatusButton.displayName = "MorphStatusButton";
export default MorphStatusButton;
MorphStatusButton
By jay sharma
Try the interaction, copy the React source files, or download the complete example.
About MorphStatusButton
MorphStatusButton is an animated React and Tailwind CSS button by jay sharma. 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/morph-status-button.tsx
import { MorphStatusButton } from "@/components/evil-buttons/morph-status-button";
export function ButtonDemo() {
return (
<MorphStatusButton
successLabel="Saved"
onClick={async () => {
await saveChanges();
}}
>
Save changes
</MorphStatusButton>
);
}
components/evil-buttons/morph-status-button.tsx
"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
export type MorphStatus = "idle" | "loading" | "success" | "error";
export interface MorphStatusButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
/** Idle label. Falls back to `label`. */
children?: React.ReactNode;
/** Idle label used when no children are provided. */
label?: React.ReactNode;
/** Label shown while the action is in flight. */
loadingLabel?: React.ReactNode;
/** Label shown after the action resolves. */
successLabel?: React.ReactNode;
/** Label shown after the action rejects. */
errorLabel?: React.ReactNode;
/**
* Click handler. If it returns a promise, the button auto-transitions
* through loading and into success or error based on the outcome.
*/
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void | Promise<unknown>;
/**
* Controlled status. When provided, the button reflects this value and the
* promise-based auto-transitions are ignored.
*/
status?: MorphStatus;
/** Milliseconds to stay in success/error before resetting. Set to 0 to stay. */
resetAfter?: number;
}
const Spinner = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
className="size-4 shrink-0 animate-spin"
aria-hidden
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
const CheckIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4 shrink-0"
aria-hidden
>
<path d="M20 6 9 17l-5-5" />
</svg>
);
const XIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4 shrink-0"
aria-hidden
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
function textOf(node: React.ReactNode): string {
if (node === null || node === undefined || typeof node === "boolean") {
return "";
}
if (typeof node === "string" || typeof node === "number") {
return String(node);
}
if (Array.isArray(node)) return node.map(textOf).join("");
if (React.isValidElement<{ children?: React.ReactNode }>(node)) {
return textOf(node.props.children);
}
return "";
}
export const MorphStatusButton = React.forwardRef<
HTMLButtonElement,
MorphStatusButtonProps
>(
(
{
children,
label = "Save changes",
loadingLabel = "Working…",
successLabel = "Done",
errorLabel = "It broke. Your fault.",
onClick,
status: controlledStatus,
resetAfter = 1800,
className,
disabled,
...props
},
ref,
) => {
const reduceMotion = useReducedMotion();
const [internal, setInternal] = React.useState<MorphStatus>("idle");
const resetTimeoutRef = React.useRef<number | null>(null);
const isControlled = controlledStatus !== undefined;
const status = isControlled ? controlledStatus : internal;
const clearReset = () => {
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
resetTimeoutRef.current = null;
}
};
React.useEffect(() => () => clearReset(), []);
const scheduleReset = () => {
if (isControlled || resetAfter <= 0) return;
clearReset();
resetTimeoutRef.current = window.setTimeout(() => {
setInternal("idle");
}, resetAfter);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (status === "loading") return;
const result = onClick?.(e);
if (isControlled || !(result instanceof Promise)) return;
clearReset();
setInternal("loading");
result
.then(() => {
setInternal("success");
scheduleReset();
})
.catch(() => {
setInternal("error");
scheduleReset();
});
};
const idleLabel = children ?? label;
const currentLabel: React.ReactNode =
status === "loading"
? loadingLabel
: status === "success"
? successLabel
: status === "error"
? errorLabel
: idleLabel;
const icon =
status === "loading" ? (
<Spinner />
) : status === "success" ? (
<CheckIcon />
) : status === "error" ? (
<XIcon />
) : null;
// Widest possible label reserves width so the button never reflows.
const widest = [
idleLabel,
loadingLabel,
successLabel,
errorLabel,
].reduce<React.ReactNode>(
(widestNode, candidate) =>
textOf(candidate).length > textOf(widestNode).length
? candidate
: widestNode,
"",
);
return (
<button
ref={ref}
type="button"
onClick={handleClick}
disabled={disabled || status === "loading"}
aria-live="polite"
aria-busy={status === "loading"}
data-state={status}
className={cn(
"relative inline-flex h-9 cursor-pointer items-center justify-center overflow-hidden rounded-[10px] px-4 text-sm font-medium text-neutral-50 outline-none select-none",
// Graded dark surface matching MinimalButton: dark outer hairline, faint inner ring, top highlight, soft drop.
"bg-linear-to-b from-[#353535] to-[#272727] shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.06),inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(0_0_0/0.4)]",
"transition-[scale,filter,box-shadow,color] duration-300 ease-[cubic-bezier(0.34,1.35,0.64,1)] hover:brightness-110",
"active:scale-[0.97] active:brightness-95 active:duration-100 active:ease-out active:shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(255_255_255/0.05),inset_0_1px_0_rgb(255_255_255/0.08),0_0_1px_rgb(0_0_0/0.2),0_1px_3px_-2px_rgb(0_0_0/0.3)] motion-reduce:active:scale-100",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
// In flight, not disabled: stay solid with a dimmed label.
status === "loading" &&
"text-neutral-50/70 hover:brightness-100 disabled:opacity-100",
// Error keeps the dark face; a red label and inner ring carry the blame.
status === "error" &&
"text-red-300 shadow-[0_0_0_1px_rgb(0_0_0/0.9),inset_0_0_0_1px_rgb(248_113_113/0.35),inset_0_1px_0_rgb(255_255_255/0.1),0_1px_2px_rgb(0_0_0/0.25),0_4px_12px_-4px_rgb(185_28_28/0.45)]",
className,
)}
{...props}
>
{/* Invisible widest label reserves a stable width. */}
<span aria-hidden className="invisible inline-flex items-center gap-2">
<span className="size-4" />
{widest}
</span>
<span className="absolute inset-0 flex items-center justify-center gap-2">
{icon ? (
<span className="relative inline-flex size-4 items-center justify-center">
<AnimatePresence initial={false} mode="wait">
<motion.span
key={status}
initial={reduceMotion ? false : { scale: 0.4, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={reduceMotion ? undefined : { scale: 0.4, opacity: 0 }}
transition={{ type: "spring", stiffness: 380, damping: 22 }}
className="inline-flex"
>
{icon}
</motion.span>
</AnimatePresence>
</span>
) : null}
<span className="relative inline-block">
<AnimatePresence initial={false} mode="wait">
<motion.span
key={status}
initial={reduceMotion ? false : { y: 8, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={reduceMotion ? undefined : { y: -8, opacity: 0 }}
transition={{ duration: 0.16 }}
className="inline-block whitespace-nowrap"
>
{currentLabel}
</motion.span>
</AnimatePresence>
</span>
</span>
</button>
);
},
);
MorphStatusButton.displayName = "MorphStatusButton";
export default MorphStatusButton;
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
demo.tsx
export { ButtonDemo as default } from "@/import-examples/morph-status-button";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--color-red-300: oklch(80.8% 0.114 19.571);
--color-neutral-50: oklch(98.5% 0 none);
--spacing: 0.25rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--font-weight-medium: 500;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--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-geist-sans), ui-sans-serif, system-ui, sans-serif;
--default-mono-font-family: var(--font-geist-mono);
}
}
@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 {
.invisible {
visibility: hidden;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-0 {
inset: 0px;
}
.flex {
display: flex;
}
.inline-block {
display: inline-block;
}
.inline-flex {
display: inline-flex;
}
.size-4 {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
.h-9 {
height: calc(var(--spacing) * 9);
}
.shrink-0 {
flex-shrink: 0;
}
.animate-spin {
animation: var(--animate-spin);
}
.cursor-pointer {
cursor: pointer;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.overflow-hidden {
overflow: hidden;
}
.rounded-\[10px\] {
border-radius: 10px;
}
.bg-linear-to-b {
--tw-gradient-position: to bottom;
@supports (background-image: linear-gradient(in lab, red, red)) {
--tw-gradient-position: to bottom in oklab;
}
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-\[\#353535\] {
--tw-gradient-from: #353535;
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-\[\#272727\] {
--tw-gradient-to: #272727;
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-neutral-50 {
color: var(--color-neutral-50);
}
.text-neutral-50\/70 {
color: color-mix(in srgb, oklch(98.5% 0 none) 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-neutral-50) 70%, transparent);
}
}
.text-red-300 {
color: var(--color-red-300);
}
.shadow-\[0_0_0_1px_rgb\(0_0_0\/0\.9\)\,inset_0_0_0_1px_rgb\(248_113_113\/0\.35\)\,inset_0_1px_0_rgb\(255_255_255\/0\.1\)\,0_1px_2px_rgb\(0_0_0\/0\.25\)\,0_4px_12px_-4px_rgb\(185_28_28\/0\.45\)\] {
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.9)), inset 0 0 0 1px var(--tw-shadow-color, rgb(248 113 113/0.35)), inset 0 1px 0 var(--tw-shadow-color, rgb(255 255 255/0.1)), 0 1px 2px var(--tw-shadow-color, rgb(0 0 0/0.25)), 0 4px 12px -4px var(--tw-shadow-color, rgb(185 28 28/0.45));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-\[0_0_0_1px_rgb\(0_0_0\/0\.9\)\,inset_0_0_0_1px_rgb\(255_255_255\/0\.06\)\,inset_0_1px_0_rgb\(255_255_255\/0\.14\)\,0_1px_2px_rgb\(0_0_0\/0\.25\)\,0_4px_12px_-4px_rgb\(0_0_0\/0\.4\)\] {
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.9)), inset 0 0 0 1px var(--tw-shadow-color, rgb(255 255 255/0.06)), inset 0 1px 0 var(--tw-shadow-color, rgb(255 255 255/0.14)), 0 1px 2px var(--tw-shadow-color, rgb(0 0 0/0.25)), 0 4px 12px -4px var(--tw-shadow-color, rgb(0 0 0/0.4));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + 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);
}
.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-\[scale\,filter\,box-shadow\,color\] {
transition-property: scale,filter,box-shadow,color;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-300 {
--tw-duration: 300ms;
transition-duration: 300ms;
}
.ease-\[cubic-bezier\(0\.34\,1\.35\,0\.64\,1\)\] {
--tw-ease: cubic-bezier(0.34,1.35,0.64,1);
transition-timing-function: cubic-bezier(0.34,1.35,0.64,1);
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
@media (hover: hover) {
.hover\:brightness-100:hover {
--tw-brightness: brightness(100%);
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,);
}
.hover\:brightness-110:hover {
--tw-brightness: brightness(110%);
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,);
}
}
.focus-visible\:ring-2:focus-visible {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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-ring:focus-visible {
--tw-ring-color: var(--ring);
}
.focus-visible\:ring-offset-2:focus-visible {
--tw-ring-offset-width: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.focus-visible\:ring-offset-background:focus-visible {
--tw-ring-offset-color: var(--background);
}
.active\:scale-\[0\.97\]:active {
scale: 0.97;
}
.active\:shadow-\[0_0_0_1px_rgb\(0_0_0\/0\.9\)\,inset_0_0_0_1px_rgb\(255_255_255\/0\.05\)\,inset_0_1px_0_rgb\(255_255_255\/0\.08\)\,0_0_1px_rgb\(0_0_0\/0\.2\)\,0_1px_3px_-2px_rgb\(0_0_0\/0\.3\)\]:active {
--tw-shadow: 0 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.9)), inset 0 0 0 1px var(--tw-shadow-color, rgb(255 255 255/0.05)), inset 0 1px 0 var(--tw-shadow-color, rgb(255 255 255/0.08)), 0 0 1px var(--tw-shadow-color, rgb(0 0 0/0.2)), 0 1px 3px -2px var(--tw-shadow-color, rgb(0 0 0/0.3));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.active\:brightness-95:active {
--tw-brightness: brightness(95%);
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,);
}
.active\:duration-100:active {
--tw-duration: 100ms;
transition-duration: 100ms;
}
.active\:ease-out:active {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.disabled\:cursor-not-allowed:disabled {
cursor: not-allowed;
}
.disabled\:opacity-50:disabled {
opacity: 50%;
}
.disabled\:opacity-100:disabled {
opacity: 100%;
}
@media (prefers-reduced-motion: reduce) {
.motion-reduce\:active\:scale-100:active {
--tw-scale-x: 100%;
--tw-scale-y: 100%;
--tw-scale-z: 100%;
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}
}
: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 {
--card: oklch(1 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--primary: oklch(0.205 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--chrome: oklch(0.967 0 0);
--brand: oklch(0.6 0.23 29);
--chart-background: oklch(1 0 0);
--chart-foreground: oklch(0.145 0.004 285);
--chart-foreground-muted: oklch(0.55 0.014 260);
--chart-line-primary: var(--chart-1);
--chart-line-secondary: var(--chart-2);
--chart-crosshair: oklch(0.4 0.1828 274.34);
--chart-grid: oklch(0.9 0 0);
--chart-brush-border: var(--chart-grid);
--chart-tooltip-background: oklch(0.21 0.006 285 / 0.8);
--chart-tooltip-foreground: oklch(0.985 0 0);
--chart-tooltip-muted: oklch(0.65 0.01 260);
--chart-marker-background: oklch(0.97 0.005 260);
--chart-marker-border: oklch(0.85 0.01 260);
--chart-marker-foreground: oklch(0.3 0.01 260);
--chart-label: oklch(0.45 0.01 260);
--chart-scale-01: oklch(0.98 0.003 106);
--chart-scale-02: oklch(0.92 0.008 106);
--chart-scale-03: oklch(0.82 0.015 106);
--chart-scale-04: oklch(0.68 0.02 106);
--chart-scale-05: oklch(0.55 0.025 106);
--chart-scale-pattern-color: oklch(0.96 0.005 106);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--chrome: oklch(0.18 0 0);
--card: oklch(0.205 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 10%);
--primary: oklch(0.922 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.922 0 0);
--sidebar-primary-foreground: oklch(0.205 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--brand: oklch(0.66 0.23 32);
--chart-background: oklch(0.145 0 0);
--chart-foreground: oklch(0.45 0 0);
--chart-foreground-muted: oklch(0.65 0.01 260);
--chart-crosshair: oklch(0.45 0 0);
--chart-grid: oklch(0.25 0 0);
--chart-brush-border: var(--chart-grid);
--chart-marker-background: oklch(0.25 0.01 260);
--chart-marker-border: oklch(0.4 0.01 260);
--chart-marker-foreground: oklch(0.9 0 0);
--chart-label: oklch(0.75 0.01 260);
--chart-scale-01: oklch(0.28 0.03 270);
--chart-scale-02: oklch(0.38 0.04 270);
--chart-scale-03: oklch(0.48 0.05 270);
--chart-scale-04: oklch(0.58 0.06 270);
--chart-scale-05: oklch(0.68 0.07 270);
--chart-scale-pattern-color: oklch(0.145 0 0);
}
@keyframes docs-dot-swing {
0%, 100% {
transform: translateX(0) scale(1);
}
45% {
transform: translateX(-6px) scale(0.7);
}
}
@keyframes hell-flicker {
0%, 100% {
opacity: 1;
}
41% {
opacity: 0.7;
}
43% {
opacity: 1;
}
77% {
opacity: 0.85;
}
78% {
opacity: 1;
}
}
@keyframes lights-off {
0% {
opacity: 0;
}
15% {
opacity: 1;
}
30% {
opacity: 0.2;
}
45% {
opacity: 1;
}
60% {
opacity: 0.5;
}
70%, 100% {
opacity: 1;
}
}
@keyframes lights-on {
0% {
opacity: 1;
}
20% {
opacity: 0;
}
32% {
opacity: 1;
}
50% {
opacity: 0.1;
}
60% {
opacity: 0.9;
}
75%, 100% {
opacity: 0;
}
}
@keyframes lights-fade {
to {
opacity: 0;
}
}
@keyframes eyes-dim {
from {
opacity: 0;
}
}
@keyframes eyes-open {
from {
opacity: 0;
transform: scaleY(0.05);
}
}
@keyframes eyes-blink {
0%, 92%, 100% {
transform: none;
}
95% {
transform: scaleY(0.08);
}
}
@keyframes crack-life {
0% {
opacity: 0;
transform: scale(0.92);
}
4% {
opacity: 1;
transform: none;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes egg-wobble {
0%, 84%, 100% {
transform: none;
}
87% {
transform: rotate(-14deg);
}
90% {
transform: rotate(10deg);
}
93% {
transform: rotate(-6deg);
}
96% {
transform: rotate(3deg);
}
}
@keyframes page-glitch {
0% {
transform: none;
filter: none;
clip-path: none;
}
12% {
transform: translate(-6px, 2px) skewX(-2deg);
filter: hue-rotate(90deg) saturate(3);
}
24% {
transform: translate(5px, -1px);
filter: invert(1) hue-rotate(180deg);
clip-path: inset(18% 0 52% 0);
}
36% {
transform: none;
filter: contrast(2.2) saturate(0);
clip-path: none;
}
48% {
transform: translate(9px, 0) skewX(4deg);
filter: hue-rotate(-60deg);
clip-path: inset(0 0 70% 0);
}
60% {
transform: translate(-3px, 3px);
filter: none;
clip-path: inset(40% 0 20% 0);
}
72% {
transform: translate(2px, -4px) scaleY(1.02);
filter: saturate(4) contrast(1.4);
clip-path: none;
}
86% {
transform: translate(-8px, 0);
filter: invert(1);
clip-path: inset(62% 0 8% 0);
}
}
@keyframes glitch-label {
0% {
transform: translate(0, 0);
opacity: 1;
}
33% {
transform: translate(-4px, 2px);
opacity: 0.6;
}
66% {
transform: translate(5px, -2px);
opacity: 1;
}
}
@keyframes deleted-progress {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes deleted-restore {
0%, 70% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes creeper-swell {
0% {
transform: scale(0.3) translateY(40px);
opacity: 0;
filter: none;
}
20% {
transform: scale(1);
opacity: 1;
}
45%, 65%, 85% {
filter: brightness(3) saturate(0);
}
55%, 75% {
filter: none;
}
100% {
transform: scale(1.35);
filter: brightness(4) saturate(0);
}
}
@keyframes creeper-flash {
from {
opacity: 0.85;
}
to {
opacity: 0;
}
}
@property --tw-gradient-position {
syntax: "*";
inherits: false;
}
@property --tw-gradient-from {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-via {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-to {
syntax: "<color>";
inherits: false;
initial-value: #0000;
}
@property --tw-gradient-stops {
syntax: "*";
inherits: false;
}
@property --tw-gradient-via-stops {
syntax: "*";
inherits: false;
}
@property --tw-gradient-from-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0%;
}
@property --tw-gradient-via-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 50%;
}
@property --tw-gradient-to-position {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
@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-duration {
syntax: "*";
inherits: false;
}
@property --tw-ease {
syntax: "*";
inherits: false;
}
@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;
}
@property --tw-scale-x {
syntax: "*";
inherits: false;
initial-value: 1;
}
@property --tw-scale-y {
syntax: "*";
inherits: false;
initial-value: 1;
}
@property --tw-scale-z {
syntax: "*";
inherits: false;
initial-value: 1;
}
@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-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
--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-duration: initial;
--tw-ease: initial;
--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;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-scale-z: 1;
}
}
}