"use client";
/**
* @author: @dorianbaffier
* @description: Undo Button
* @version: 1.0.0
* @date: 2026-10-16
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import {Archive,Check,type LucideIcon,Undo2} from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
type PointerEvent,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
const EASE_OUT = [0.23, 1, 0.32, 1] as const;
const STROKE = 1.5;
const APPLE_PLATFORM = /Mac|iPhone|iPad/;
type Phase = "idle" | "pending" | "done";
export interface UndoButtonProps {
/** What the button does, e.g. "Archive". */
label?: string;
/** The same action in the past tense, e.g. "Archived". */
doneLabel?: string;
icon?: LucideIcon;
/** How long the action can be undone, in ms. */
duration?: number;
/**
* After the action commits, go back to the start after this many ms.
* Pass `null` to stay on the done state.
*/
resetAfter?: number | null;
/** Called when the button is pressed and the undo window opens. */
onAction?: () => void;
/** Called when the window closes without an undo: do the real work here. */
onCommit?: () => void;
onUndo?: () => void;
className?: string;
}
// A pill outline that starts at the top centre and runs clockwise, so the
// countdown drains back toward where it began.
function pillPath(width: number, height: number) {
const s = STROKE / 2;
const r = height / 2 - s;
const left = s + r;
const right = width - s - r;
return [
`M ${width / 2} ${s}`,
`H ${right}`,
`A ${r} ${r} 0 0 1 ${right} ${height - s}`,
`H ${left}`,
`A ${r} ${r} 0 0 1 ${left} ${s}`,
"Z",
].join(" ");
}
const isEditable = (target: EventTarget | null) =>
target instanceof HTMLElement &&
(target.isContentEditable ||
["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
export default function UndoButton({
label = "Archive",
doneLabel = "Archived",
icon: Icon = Archive,
duration = 5000,
resetAfter = 2400,
onAction,
onCommit,
onUndo,
className,
}: UndoButtonProps) {
const [phase, setPhase] = useState<Phase>("idle");
const [seconds, setSeconds] = useState(Math.ceil(duration / 1000));
const [width, setWidth] = useState<number>();
const [isMac, setIsMac] = useState(true);
const reduceMotion = useReducedMotion();
const buttonRef = useRef<HTMLButtonElement>(null);
const contentRef = useRef<HTMLSpanElement>(null);
const pathRef = useRef<SVGPathElement>(null);
const hoveredRef = useRef(false);
const elapsedRef = useRef(0);
const remainingRef = useRef(1);
const callbacks = useRef({ onAction, onCommit, onUndo });
callbacks.current = { onAction, onCommit, onUndo };
useEffect(() => {
setIsMac(APPLE_PLATFORM.test(navigator.platform));
}, []);
// The button's width follows its label, so the pill grows into
// "Archived | Undo 5" instead of jumping.
useLayoutEffect(() => {
const content = contentRef.current;
if (!content) {
return;
}
// +2 for the 1px border on each side.
const observer = new ResizeObserver(() =>
setWidth(content.offsetWidth + 2)
);
observer.observe(content);
return () => observer.disconnect();
}, []);
// Dashes in real lengths rather than pathLength, which Chrome scales
// inconsistently once the path changes size.
const paint = useCallback(() => {
const path = pathRef.current;
if (!path) {
return;
}
const length = path.getTotalLength();
path.style.strokeDasharray = `${length * remainingRef.current} ${length}`;
}, []);
// Keep the outline glued to the button while its width animates.
useLayoutEffect(() => {
const button = buttonRef.current;
if (!button) {
return;
}
const observer = new ResizeObserver(() => {
pathRef.current?.setAttribute(
"d",
pillPath(button.offsetWidth, button.offsetHeight)
);
paint();
});
observer.observe(button);
return () => observer.disconnect();
}, [paint]);
const undo = useCallback(() => {
setPhase("idle");
callbacks.current.onUndo?.();
}, []);
useEffect(() => {
if (phase !== "pending") {
return;
}
elapsedRef.current = 0;
remainingRef.current = 1;
paint();
setSeconds(Math.ceil(duration / 1000));
let frame = 0;
let last = performance.now();
const tick = (now: number) => {
// Hovering the button, or leaving the tab, holds the clock: nobody
// should lose their chance to undo while reading it.
if (!(hoveredRef.current || document.hidden)) {
elapsedRef.current += now - last;
}
last = now;
const left = Math.max(0, duration - elapsedRef.current);
remainingRef.current = left / duration;
paint();
setSeconds(Math.ceil(left / 1000));
if (left === 0) {
setPhase("done");
callbacks.current.onCommit?.();
return;
}
frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
const onKeyDown = (event: KeyboardEvent) => {
const mod = event.metaKey || event.ctrlKey;
if (
mod &&
!event.shiftKey &&
event.key.toLowerCase() === "z" &&
!isEditable(event.target)
) {
event.preventDefault();
undo();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener("keydown", onKeyDown);
};
}, [phase, duration, paint, undo]);
useEffect(() => {
if (phase !== "done" || resetAfter === null) {
return;
}
const timer = setTimeout(() => setPhase("idle"), resetAfter);
return () => clearTimeout(timer);
}, [phase, resetAfter]);
const handleClick = () => {
if (phase === "idle") {
setPhase("pending");
callbacks.current.onAction?.();
} else if (phase === "pending") {
undo();
}
};
const setHovered = (event: PointerEvent, value: boolean) => {
if (event.pointerType === "mouse") {
hoveredRef.current = value;
}
};
const swap = reduceMotion
? { duration: 0 }
: { duration: 0.22, ease: EASE_OUT };
const ariaLabel = {
idle: label,
pending: `${doneLabel}. Undo, ${seconds} second${seconds === 1 ? "" : "s"} left`,
done: doneLabel,
}[phase];
return (
<span className="relative inline-flex">
<motion.button
animate={{ width: width ?? "auto" }}
aria-disabled={phase === "done"}
aria-label={ariaLabel}
className={cn(
"relative inline-flex h-10 select-none items-center justify-center overflow-hidden whitespace-nowrap rounded-full border font-medium text-sm outline-none",
"transition-[background-color,border-color,color] duration-150 ease-out",
"focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-zinc-950",
phase === "done"
? "cursor-default border-zinc-200 bg-zinc-50 text-zinc-500 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400"
: "border-zinc-200 bg-white text-zinc-900 hover:bg-zinc-50 active:bg-zinc-100 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100 dark:active:bg-zinc-800 dark:hover:bg-zinc-900",
className
)}
initial={false}
onClick={handleClick}
onPointerEnter={(event) => setHovered(event, true)}
onPointerLeave={(event) => setHovered(event, false)}
ref={buttonRef}
transition={swap}
type="button"
>
<span className="inline-flex px-4" ref={contentRef}>
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
className="inline-flex items-center gap-2"
exit={{ opacity: 0, y: -8, filter: "blur(2px)" }}
initial={{ opacity: 0, y: 8, filter: "blur(2px)" }}
key={phase}
transition={swap}
>
{phase === "idle" && (
<>
<Icon aria-hidden className="size-4" />
{label}
</>
)}
{phase === "pending" && (
<>
<span className="text-zinc-500 dark:text-zinc-400">
{doneLabel}
</span>
<span
aria-hidden
className="h-4 w-px bg-zinc-200 dark:bg-zinc-800"
/>
<Undo2 aria-hidden className="size-4" />
Undo
<Countdown reduceMotion={!!reduceMotion} value={seconds} />
<kbd className="hidden rounded border border-zinc-200 px-1 font-sans text-[11px] text-zinc-500 sm:inline dark:border-zinc-800 dark:text-zinc-400">
{isMac ? "⌘Z" : "Ctrl Z"}
</kbd>
</>
)}
{phase === "done" && (
<>
<Check aria-hidden className="size-4" />
{doneLabel}
</>
)}
</motion.span>
</AnimatePresence>
</span>
</motion.button>
<svg
aria-hidden
className="pointer-events-none absolute inset-0 size-full overflow-visible"
>
<title>Time left to undo</title>
<motion.path
animate={{ opacity: phase === "pending" ? 1 : 0 }}
className="stroke-zinc-900 dark:stroke-zinc-100"
fill="none"
initial={false}
ref={pathRef}
strokeLinecap="round"
strokeWidth={STROKE}
transition={{ duration: phase === "pending" ? 0.15 : 0.2 }}
/>
</svg>
<span aria-live="polite" className="sr-only">
{phase === "pending" &&
`${doneLabel}. Press the button or ${isMac ? "Command" : "Control"} Z to undo.`}
{phase === "done" && doneLabel}
</span>
</span>
);
}
function Countdown({
value,
reduceMotion,
}: {
value: number;
reduceMotion: boolean;
}) {
return (
<span className="relative inline-flex h-5 min-w-[1ch] justify-center overflow-hidden text-zinc-500 tabular-nums dark:text-zinc-400">
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={{ y: 0, opacity: 1 }}
className="leading-5"
exit={{ y: -10, opacity: 0 }}
initial={{ y: 10, opacity: 0 }}
key={value}
transition={
reduceMotion ? { duration: 0 } : { duration: 0.2, ease: EASE_OUT }
}
>
{value}
</motion.span>
</AnimatePresence>
</span>
);
}
Undo Button
By Kokonut UI
Try the interaction, copy the React source files, or download the complete example.
About Undo Button
Undo Button is an animated React and Tailwind CSS button by Kokonut UI. 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/undo-button.tsx
import UndoButton from "@/components/kokonutui/undo-button";
export default function ImportedDemo(){return <>
<UndoButton />
</>}components/kokonutui/undo-button.tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Undo Button
* @version: 1.0.0
* @date: 2026-10-16
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import {Archive,Check,type LucideIcon,Undo2} from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
type PointerEvent,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
const EASE_OUT = [0.23, 1, 0.32, 1] as const;
const STROKE = 1.5;
const APPLE_PLATFORM = /Mac|iPhone|iPad/;
type Phase = "idle" | "pending" | "done";
export interface UndoButtonProps {
/** What the button does, e.g. "Archive". */
label?: string;
/** The same action in the past tense, e.g. "Archived". */
doneLabel?: string;
icon?: LucideIcon;
/** How long the action can be undone, in ms. */
duration?: number;
/**
* After the action commits, go back to the start after this many ms.
* Pass `null` to stay on the done state.
*/
resetAfter?: number | null;
/** Called when the button is pressed and the undo window opens. */
onAction?: () => void;
/** Called when the window closes without an undo: do the real work here. */
onCommit?: () => void;
onUndo?: () => void;
className?: string;
}
// A pill outline that starts at the top centre and runs clockwise, so the
// countdown drains back toward where it began.
function pillPath(width: number, height: number) {
const s = STROKE / 2;
const r = height / 2 - s;
const left = s + r;
const right = width - s - r;
return [
`M ${width / 2} ${s}`,
`H ${right}`,
`A ${r} ${r} 0 0 1 ${right} ${height - s}`,
`H ${left}`,
`A ${r} ${r} 0 0 1 ${left} ${s}`,
"Z",
].join(" ");
}
const isEditable = (target: EventTarget | null) =>
target instanceof HTMLElement &&
(target.isContentEditable ||
["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
export default function UndoButton({
label = "Archive",
doneLabel = "Archived",
icon: Icon = Archive,
duration = 5000,
resetAfter = 2400,
onAction,
onCommit,
onUndo,
className,
}: UndoButtonProps) {
const [phase, setPhase] = useState<Phase>("idle");
const [seconds, setSeconds] = useState(Math.ceil(duration / 1000));
const [width, setWidth] = useState<number>();
const [isMac, setIsMac] = useState(true);
const reduceMotion = useReducedMotion();
const buttonRef = useRef<HTMLButtonElement>(null);
const contentRef = useRef<HTMLSpanElement>(null);
const pathRef = useRef<SVGPathElement>(null);
const hoveredRef = useRef(false);
const elapsedRef = useRef(0);
const remainingRef = useRef(1);
const callbacks = useRef({ onAction, onCommit, onUndo });
callbacks.current = { onAction, onCommit, onUndo };
useEffect(() => {
setIsMac(APPLE_PLATFORM.test(navigator.platform));
}, []);
// The button's width follows its label, so the pill grows into
// "Archived | Undo 5" instead of jumping.
useLayoutEffect(() => {
const content = contentRef.current;
if (!content) {
return;
}
// +2 for the 1px border on each side.
const observer = new ResizeObserver(() =>
setWidth(content.offsetWidth + 2)
);
observer.observe(content);
return () => observer.disconnect();
}, []);
// Dashes in real lengths rather than pathLength, which Chrome scales
// inconsistently once the path changes size.
const paint = useCallback(() => {
const path = pathRef.current;
if (!path) {
return;
}
const length = path.getTotalLength();
path.style.strokeDasharray = `${length * remainingRef.current} ${length}`;
}, []);
// Keep the outline glued to the button while its width animates.
useLayoutEffect(() => {
const button = buttonRef.current;
if (!button) {
return;
}
const observer = new ResizeObserver(() => {
pathRef.current?.setAttribute(
"d",
pillPath(button.offsetWidth, button.offsetHeight)
);
paint();
});
observer.observe(button);
return () => observer.disconnect();
}, [paint]);
const undo = useCallback(() => {
setPhase("idle");
callbacks.current.onUndo?.();
}, []);
useEffect(() => {
if (phase !== "pending") {
return;
}
elapsedRef.current = 0;
remainingRef.current = 1;
paint();
setSeconds(Math.ceil(duration / 1000));
let frame = 0;
let last = performance.now();
const tick = (now: number) => {
// Hovering the button, or leaving the tab, holds the clock: nobody
// should lose their chance to undo while reading it.
if (!(hoveredRef.current || document.hidden)) {
elapsedRef.current += now - last;
}
last = now;
const left = Math.max(0, duration - elapsedRef.current);
remainingRef.current = left / duration;
paint();
setSeconds(Math.ceil(left / 1000));
if (left === 0) {
setPhase("done");
callbacks.current.onCommit?.();
return;
}
frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
const onKeyDown = (event: KeyboardEvent) => {
const mod = event.metaKey || event.ctrlKey;
if (
mod &&
!event.shiftKey &&
event.key.toLowerCase() === "z" &&
!isEditable(event.target)
) {
event.preventDefault();
undo();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
cancelAnimationFrame(frame);
window.removeEventListener("keydown", onKeyDown);
};
}, [phase, duration, paint, undo]);
useEffect(() => {
if (phase !== "done" || resetAfter === null) {
return;
}
const timer = setTimeout(() => setPhase("idle"), resetAfter);
return () => clearTimeout(timer);
}, [phase, resetAfter]);
const handleClick = () => {
if (phase === "idle") {
setPhase("pending");
callbacks.current.onAction?.();
} else if (phase === "pending") {
undo();
}
};
const setHovered = (event: PointerEvent, value: boolean) => {
if (event.pointerType === "mouse") {
hoveredRef.current = value;
}
};
const swap = reduceMotion
? { duration: 0 }
: { duration: 0.22, ease: EASE_OUT };
const ariaLabel = {
idle: label,
pending: `${doneLabel}. Undo, ${seconds} second${seconds === 1 ? "" : "s"} left`,
done: doneLabel,
}[phase];
return (
<span className="relative inline-flex">
<motion.button
animate={{ width: width ?? "auto" }}
aria-disabled={phase === "done"}
aria-label={ariaLabel}
className={cn(
"relative inline-flex h-10 select-none items-center justify-center overflow-hidden whitespace-nowrap rounded-full border font-medium text-sm outline-none",
"transition-[background-color,border-color,color] duration-150 ease-out",
"focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-zinc-950",
phase === "done"
? "cursor-default border-zinc-200 bg-zinc-50 text-zinc-500 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-400"
: "border-zinc-200 bg-white text-zinc-900 hover:bg-zinc-50 active:bg-zinc-100 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100 dark:active:bg-zinc-800 dark:hover:bg-zinc-900",
className
)}
initial={false}
onClick={handleClick}
onPointerEnter={(event) => setHovered(event, true)}
onPointerLeave={(event) => setHovered(event, false)}
ref={buttonRef}
transition={swap}
type="button"
>
<span className="inline-flex px-4" ref={contentRef}>
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
className="inline-flex items-center gap-2"
exit={{ opacity: 0, y: -8, filter: "blur(2px)" }}
initial={{ opacity: 0, y: 8, filter: "blur(2px)" }}
key={phase}
transition={swap}
>
{phase === "idle" && (
<>
<Icon aria-hidden className="size-4" />
{label}
</>
)}
{phase === "pending" && (
<>
<span className="text-zinc-500 dark:text-zinc-400">
{doneLabel}
</span>
<span
aria-hidden
className="h-4 w-px bg-zinc-200 dark:bg-zinc-800"
/>
<Undo2 aria-hidden className="size-4" />
Undo
<Countdown reduceMotion={!!reduceMotion} value={seconds} />
<kbd className="hidden rounded border border-zinc-200 px-1 font-sans text-[11px] text-zinc-500 sm:inline dark:border-zinc-800 dark:text-zinc-400">
{isMac ? "⌘Z" : "Ctrl Z"}
</kbd>
</>
)}
{phase === "done" && (
<>
<Check aria-hidden className="size-4" />
{doneLabel}
</>
)}
</motion.span>
</AnimatePresence>
</span>
</motion.button>
<svg
aria-hidden
className="pointer-events-none absolute inset-0 size-full overflow-visible"
>
<title>Time left to undo</title>
<motion.path
animate={{ opacity: phase === "pending" ? 1 : 0 }}
className="stroke-zinc-900 dark:stroke-zinc-100"
fill="none"
initial={false}
ref={pathRef}
strokeLinecap="round"
strokeWidth={STROKE}
transition={{ duration: phase === "pending" ? 0.15 : 0.2 }}
/>
</svg>
<span aria-live="polite" className="sr-only">
{phase === "pending" &&
`${doneLabel}. Press the button or ${isMac ? "Command" : "Control"} Z to undo.`}
{phase === "done" && doneLabel}
</span>
</span>
);
}
function Countdown({
value,
reduceMotion,
}: {
value: number;
reduceMotion: boolean;
}) {
return (
<span className="relative inline-flex h-5 min-w-[1ch] justify-center overflow-hidden text-zinc-500 tabular-nums dark:text-zinc-400">
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={{ y: 0, opacity: 1 }}
className="leading-5"
exit={{ y: -10, opacity: 0 }}
initial={{ y: 10, opacity: 0 }}
key={value}
transition={
reduceMotion ? { duration: 0 } : { duration: 0.2, ease: EASE_OUT }
}
>
{value}
</motion.span>
</AnimatePresence>
</span>
);
}
lib/utils.ts
import { type ClassValue, 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/undo-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;
--color-zinc-50: oklch(98.5% 0 none);
--color-zinc-100: oklch(96.7% 0.001 286.375);
--color-zinc-200: oklch(92% 0.004 286.32);
--color-zinc-400: oklch(70.5% 0.015 286.067);
--color-zinc-500: oklch(55.2% 0.016 285.938);
--color-zinc-800: oklch(27.4% 0.006 286.033);
--color-zinc-900: oklch(21% 0.006 285.885);
--color-zinc-950: oklch(14.1% 0.005 285.823);
--color-white: #fff;
--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);
--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);
}
}
@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;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border-width: 0;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-0 {
inset: 0px;
}
.hidden {
display: none;
}
.inline-flex {
display: inline-flex;
}
.size-4 {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
.size-full {
width: 100%;
height: 100%;
}
.h-4 {
height: calc(var(--spacing) * 4);
}
.h-5 {
height: calc(var(--spacing) * 5);
}
.h-10 {
height: calc(var(--spacing) * 10);
}
.w-px {
width: 1px;
}
.min-w-\[1ch\] {
min-width: 1ch;
}
.cursor-default {
cursor: default;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.overflow-hidden {
overflow: hidden;
}
.overflow-visible {
overflow: visible;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: calc(infinity * 1px);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-zinc-200 {
border-color: var(--color-zinc-200);
}
.bg-white {
background-color: var(--color-white);
}
.bg-zinc-50 {
background-color: var(--color-zinc-50);
}
.bg-zinc-200 {
background-color: var(--color-zinc-200);
}
.stroke-zinc-900 {
stroke: var(--color-zinc-900);
}
.px-1 {
padding-inline: var(--spacing);
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.font-sans {
font-family: var(--font-sans);
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.text-\[11px\] {
font-size: 11px;
}
.leading-5 {
--tw-leading: calc(var(--spacing) * 5);
line-height: calc(var(--spacing) * 5);
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-zinc-500 {
color: var(--color-zinc-500);
}
.text-zinc-900 {
color: var(--color-zinc-900);
}
.tabular-nums {
--tw-numeric-spacing: tabular-nums;
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
}
.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\] {
transition-property: background-color,border-color,color;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-150 {
--tw-duration: 150ms;
transition-duration: 150ms;
}
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
@media (hover: hover) {
.hover\:bg-zinc-50:hover {
background-color: var(--color-zinc-50);
}
}
.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-zinc-400:focus-visible {
--tw-ring-color: var(--color-zinc-400);
}
.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);
}
.active\:bg-zinc-100:active {
background-color: var(--color-zinc-100);
}
@media (width >= 40rem) {
.sm\:inline {
display: inline;
}
}
.dark\:border-zinc-800:where(.dark, .dark *) {
border-color: var(--color-zinc-800);
}
.dark\:bg-zinc-800:where(.dark, .dark *) {
background-color: var(--color-zinc-800);
}
.dark\:bg-zinc-900:where(.dark, .dark *) {
background-color: var(--color-zinc-900);
}
.dark\:bg-zinc-950:where(.dark, .dark *) {
background-color: var(--color-zinc-950);
}
.dark\:stroke-zinc-100:where(.dark, .dark *) {
stroke: var(--color-zinc-100);
}
.dark\:text-zinc-100:where(.dark, .dark *) {
color: var(--color-zinc-100);
}
.dark\:text-zinc-400:where(.dark, .dark *) {
color: var(--color-zinc-400);
}
@media (hover: hover) {
.dark\:hover\:bg-zinc-900:where(.dark, .dark *):hover {
background-color: var(--color-zinc-900);
}
}
.dark\:focus-visible\:ring-offset-zinc-950:where(.dark, .dark *):focus-visible {
--tw-ring-offset-color: var(--color-zinc-950);
}
.dark\:active\:bg-zinc-800:where(.dark, .dark *):active {
background-color: var(--color-zinc-800);
}
}
: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;
--color-fd-background: #000;
--color-fd-card: #0a0a0a;
--color-fd-popover: #0a0a0a;
--color-fd-border: rgb(255 255 255 / 0.14);
--color-fd-accent: rgb(255 255 255 / 0.07);
}
:root {
--sidebar: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
}
.dark {
--sidebar: hsl(240 5.9% 10%);
--sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
}
:root {
--leaf: #1fa834;
}
.dark {
--leaf: #3bb44b;
}
@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-ordinal {
syntax: "*";
inherits: false;
}
@property --tw-slashed-zero {
syntax: "*";
inherits: false;
}
@property --tw-numeric-figure {
syntax: "*";
inherits: false;
}
@property --tw-numeric-spacing {
syntax: "*";
inherits: false;
}
@property --tw-numeric-fraction {
syntax: "*";
inherits: false;
}
@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;
}
@property --tw-duration {
syntax: "*";
inherits: false;
}
@property --tw-ease {
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;
}
@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-border-style: solid;
--tw-leading: initial;
--tw-font-weight: initial;
--tw-ordinal: initial;
--tw-slashed-zero: initial;
--tw-numeric-figure: initial;
--tw-numeric-spacing: initial;
--tw-numeric-fraction: initial;
--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;
--tw-duration: initial;
--tw-ease: 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;
}
}
}