"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
const EASE = [0.23, 1, 0.32, 1] as const;
const CELL = { type: "spring", stiffness: 520, damping: 34, mass: 0.45 } as const;
const CROSSFADE = { type: "spring", stiffness: 260, damping: 34, mass: 0.8 } as const;
const DRAW = { duration: 0.26, ease: EASE } as const;
const INSTANT = { duration: 0 } as const;
export type CopyStatus = "idle" | "copied" | "error";
export type UseCopyToClipboardOptions = {
timeout?: number;
onCopy?: (value: string) => void;
onError?: (reason: unknown) => void;
};
function writeFallback(text: string): boolean {
const area = document.createElement("textarea");
area.value = text;
area.setAttribute("readonly", "");
area.style.position = "fixed";
area.style.top = "0";
area.style.left = "0";
area.style.opacity = "0";
document.body.appendChild(area);
const selection = document.getSelection();
const previous =
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
area.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch {
ok = false;
}
document.body.removeChild(area);
if (selection && previous) {
selection.removeAllRanges();
selection.addRange(previous);
}
return ok;
}
export function useCopyToClipboard({
timeout = 2000,
onCopy,
onError,
}: UseCopyToClipboardOptions = {}) {
const [status, setStatus] = useState<CopyStatus>("idle");
const [ticket, setTicket] = useState(0);
const mounted = useRef(true);
const copied = useRef(onCopy);
copied.current = onCopy;
const failed = useRef(onError);
failed.current = onError;
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const reset = useCallback(() => {
setStatus("idle");
setTicket(0);
}, []);
const copy = useCallback(async (text: string) => {
if (!text) return false;
let ok = false;
let reason: unknown = null;
try {
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
ok = true;
} else {
ok = writeFallback(text);
}
} catch (error) {
reason = error;
try {
ok = writeFallback(text);
} catch {
ok = false;
}
}
if (!mounted.current) return ok;
setStatus(ok ? "copied" : "error");
setTicket((t) => t + 1);
if (ok) copied.current?.(text);
else failed.current?.(reason);
return ok;
}, []);
useEffect(() => {
if (ticket === 0 || status === "idle") return;
const id = setTimeout(() => setStatus("idle"), timeout);
return () => clearTimeout(id);
}, [ticket, status, timeout]);
return { copy, reset, status, copied: status === "copied" };
}
export type CopyButtonProps = {
value: string;
label?: string;
copiedLabel?: string;
errorLabel?: string;
timeout?: number;
onCopy?: (value: string) => void;
onError?: (reason: unknown) => void;
disabled?: boolean;
className?: string;
};
export function CopyButton({
value,
label = "Copy",
copiedLabel = "Copied",
errorLabel = "Failed",
timeout = 2000,
onCopy,
onError,
disabled = false,
className = "",
}: CopyButtonProps) {
const { copy, status } = useCopyToClipboard({ timeout, onCopy, onError });
const reduced = useReducedMotion();
const fade = reduced ? INSTANT : CROSSFADE;
const draw = reduced ? INSTANT : DRAW;
const labels: Array<[CopyStatus, string]> = [
["idle", label],
["copied", copiedLabel],
["error", errorLabel],
];
return (
<motion.button
type="button"
disabled={disabled}
aria-label={label}
onClick={() => {
void copy(value);
}}
whileTap={disabled || reduced ? undefined : { y: 1 }}
transition={CELL}
style={{ borderRadius: 9, touchAction: "manipulation" }}
className={`inline-flex h-9 select-none items-center gap-2 rounded-[9px] border border-stone-200 bg-white px-3 text-[13px] font-medium text-stone-700 shadow-[inset_0_1.5px_0_rgba(255,255,255,0.95),inset_0_-1px_0_rgba(28,25,23,0.06),0_1px_2px_rgba(28,25,23,0.08)] outline-none transition-[border-color,box-shadow,background-color] duration-150 hover:bg-stone-50 focus-visible:border-[#4568FF] focus-visible:shadow-[0_1px_2px_rgba(28,25,23,0.08),0_10px_20px_-14px_rgba(69,104,255,0.6)] disabled:opacity-50 dark:border-white/[0.16] dark:bg-[#252522] dark:text-stone-200 dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.07),0_1px_2px_rgba(0,0,0,0.4)] dark:hover:bg-[#2A2A27] dark:focus-visible:border-[#93B0FF] dark:focus-visible:shadow-[0_10px_20px_-14px_rgba(147,176,255,0.5)] ${className}`}
>
<span className="grid size-[14px] shrink-0" aria-hidden="true">
<motion.svg
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="col-start-1 row-start-1 size-[14px]"
initial={false}
animate={{
opacity: status === "idle" ? 1 : 0,
scale: status === "idle" ? 1 : 0.92,
}}
transition={fade}
>
<path d="M9.6 5.1V3.7A1.7 1.7 0 0 0 7.9 2H3.7A1.7 1.7 0 0 0 2 3.7v4.2a1.7 1.7 0 0 0 1.7 1.7h1.4" />
<rect x="5.1" y="5.1" width="6.9" height="6.9" rx="1.7" />
</motion.svg>
<motion.svg
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="col-start-1 row-start-1 size-[14px]"
initial={false}
animate={{
opacity: status === "copied" ? 1 : 0,
scale: status === "copied" ? 1 : 0.92,
}}
transition={fade}
>
<motion.path
d="M2.9 7.4 5.6 10.1 11.1 4"
initial={false}
animate={{ pathLength: status === "copied" ? 1 : 0 }}
transition={draw}
/>
</motion.svg>
<motion.svg
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="col-start-1 row-start-1 size-[14px]"
initial={false}
animate={{
opacity: status === "error" ? 1 : 0,
scale: status === "error" ? 1 : 0.92,
}}
transition={fade}
>
<path d="M3.6 3.6 10.4 10.4" />
<path d="M10.4 3.6 3.6 10.4" />
</motion.svg>
</span>
<span aria-hidden="true" className="relative grid">
{labels.map(([key, text]) => (
<motion.span
key={key}
initial={false}
animate={
key === status
? { opacity: 1, y: 0, filter: "blur(0px)" }
: { opacity: 0, y: 3, filter: "blur(3px)" }
}
transition={fade}
className="col-start-1 row-start-1 whitespace-nowrap"
>
{text}
</motion.span>
))}
</span>
<span role="status" aria-live="polite" className="sr-only">
{status === "copied" ? copiedLabel : status === "error" ? errorLabel : ""}
</span>
</motion.button>
);
}
Copy Button Demo
By Özer
Try the interaction, copy the React source files, or download the complete example.
About Copy Button Demo
Copy Button Demo is an animated React and Tailwind CSS button by Özer. 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
lib/demos/copy-button-demo.tsx
"use client";
import { CopyButton } from "@/components/interior/copy-button";
export function CopyButtonDemo() {
return (
<div className="flex justify-center">
<CopyButton value="npx interior add copy-button" />
</div>
);
}
components/interior/copy-button.tsx
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
const EASE = [0.23, 1, 0.32, 1] as const;
const CELL = { type: "spring", stiffness: 520, damping: 34, mass: 0.45 } as const;
const CROSSFADE = { type: "spring", stiffness: 260, damping: 34, mass: 0.8 } as const;
const DRAW = { duration: 0.26, ease: EASE } as const;
const INSTANT = { duration: 0 } as const;
export type CopyStatus = "idle" | "copied" | "error";
export type UseCopyToClipboardOptions = {
timeout?: number;
onCopy?: (value: string) => void;
onError?: (reason: unknown) => void;
};
function writeFallback(text: string): boolean {
const area = document.createElement("textarea");
area.value = text;
area.setAttribute("readonly", "");
area.style.position = "fixed";
area.style.top = "0";
area.style.left = "0";
area.style.opacity = "0";
document.body.appendChild(area);
const selection = document.getSelection();
const previous =
selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
area.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch {
ok = false;
}
document.body.removeChild(area);
if (selection && previous) {
selection.removeAllRanges();
selection.addRange(previous);
}
return ok;
}
export function useCopyToClipboard({
timeout = 2000,
onCopy,
onError,
}: UseCopyToClipboardOptions = {}) {
const [status, setStatus] = useState<CopyStatus>("idle");
const [ticket, setTicket] = useState(0);
const mounted = useRef(true);
const copied = useRef(onCopy);
copied.current = onCopy;
const failed = useRef(onError);
failed.current = onError;
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const reset = useCallback(() => {
setStatus("idle");
setTicket(0);
}, []);
const copy = useCallback(async (text: string) => {
if (!text) return false;
let ok = false;
let reason: unknown = null;
try {
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
ok = true;
} else {
ok = writeFallback(text);
}
} catch (error) {
reason = error;
try {
ok = writeFallback(text);
} catch {
ok = false;
}
}
if (!mounted.current) return ok;
setStatus(ok ? "copied" : "error");
setTicket((t) => t + 1);
if (ok) copied.current?.(text);
else failed.current?.(reason);
return ok;
}, []);
useEffect(() => {
if (ticket === 0 || status === "idle") return;
const id = setTimeout(() => setStatus("idle"), timeout);
return () => clearTimeout(id);
}, [ticket, status, timeout]);
return { copy, reset, status, copied: status === "copied" };
}
export type CopyButtonProps = {
value: string;
label?: string;
copiedLabel?: string;
errorLabel?: string;
timeout?: number;
onCopy?: (value: string) => void;
onError?: (reason: unknown) => void;
disabled?: boolean;
className?: string;
};
export function CopyButton({
value,
label = "Copy",
copiedLabel = "Copied",
errorLabel = "Failed",
timeout = 2000,
onCopy,
onError,
disabled = false,
className = "",
}: CopyButtonProps) {
const { copy, status } = useCopyToClipboard({ timeout, onCopy, onError });
const reduced = useReducedMotion();
const fade = reduced ? INSTANT : CROSSFADE;
const draw = reduced ? INSTANT : DRAW;
const labels: Array<[CopyStatus, string]> = [
["idle", label],
["copied", copiedLabel],
["error", errorLabel],
];
return (
<motion.button
type="button"
disabled={disabled}
aria-label={label}
onClick={() => {
void copy(value);
}}
whileTap={disabled || reduced ? undefined : { y: 1 }}
transition={CELL}
style={{ borderRadius: 9, touchAction: "manipulation" }}
className={`inline-flex h-9 select-none items-center gap-2 rounded-[9px] border border-stone-200 bg-white px-3 text-[13px] font-medium text-stone-700 shadow-[inset_0_1.5px_0_rgba(255,255,255,0.95),inset_0_-1px_0_rgba(28,25,23,0.06),0_1px_2px_rgba(28,25,23,0.08)] outline-none transition-[border-color,box-shadow,background-color] duration-150 hover:bg-stone-50 focus-visible:border-[#4568FF] focus-visible:shadow-[0_1px_2px_rgba(28,25,23,0.08),0_10px_20px_-14px_rgba(69,104,255,0.6)] disabled:opacity-50 dark:border-white/[0.16] dark:bg-[#252522] dark:text-stone-200 dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.07),0_1px_2px_rgba(0,0,0,0.4)] dark:hover:bg-[#2A2A27] dark:focus-visible:border-[#93B0FF] dark:focus-visible:shadow-[0_10px_20px_-14px_rgba(147,176,255,0.5)] ${className}`}
>
<span className="grid size-[14px] shrink-0" aria-hidden="true">
<motion.svg
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="col-start-1 row-start-1 size-[14px]"
initial={false}
animate={{
opacity: status === "idle" ? 1 : 0,
scale: status === "idle" ? 1 : 0.92,
}}
transition={fade}
>
<path d="M9.6 5.1V3.7A1.7 1.7 0 0 0 7.9 2H3.7A1.7 1.7 0 0 0 2 3.7v4.2a1.7 1.7 0 0 0 1.7 1.7h1.4" />
<rect x="5.1" y="5.1" width="6.9" height="6.9" rx="1.7" />
</motion.svg>
<motion.svg
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="col-start-1 row-start-1 size-[14px]"
initial={false}
animate={{
opacity: status === "copied" ? 1 : 0,
scale: status === "copied" ? 1 : 0.92,
}}
transition={fade}
>
<motion.path
d="M2.9 7.4 5.6 10.1 11.1 4"
initial={false}
animate={{ pathLength: status === "copied" ? 1 : 0 }}
transition={draw}
/>
</motion.svg>
<motion.svg
viewBox="0 0 14 14"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="col-start-1 row-start-1 size-[14px]"
initial={false}
animate={{
opacity: status === "error" ? 1 : 0,
scale: status === "error" ? 1 : 0.92,
}}
transition={fade}
>
<path d="M3.6 3.6 10.4 10.4" />
<path d="M10.4 3.6 3.6 10.4" />
</motion.svg>
</span>
<span aria-hidden="true" className="relative grid">
{labels.map(([key, text]) => (
<motion.span
key={key}
initial={false}
animate={
key === status
? { opacity: 1, y: 0, filter: "blur(0px)" }
: { opacity: 0, y: 3, filter: "blur(3px)" }
}
transition={fade}
className="col-start-1 row-start-1 whitespace-nowrap"
>
{text}
</motion.span>
))}
</span>
<span role="status" aria-live="polite" className="sr-only">
{status === "copied" ? copiedLabel : status === "error" ? errorLabel : ""}
</span>
</motion.button>
);
}
demo.tsx
export { CopyButtonDemo as default } from "@/lib/demos/copy-button-demo";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--color-stone-50: oklch(98.5% 0.001 106.423);
--color-stone-200: oklch(92.3% 0.003 48.717);
--color-stone-700: oklch(37.4% 0.01 67.558);
--color-white: #fff;
--spacing: 0.25rem;
--font-weight-medium: 500;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-geist-sans);
--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 {
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border-width: 0;
}
.fixed {
position: fixed;
}
.relative {
position: relative;
}
.col-start-1 {
grid-column-start: 1;
}
.row-start-1 {
grid-row-start: 1;
}
.flex {
display: flex;
}
.grid {
display: grid;
}
.inline-flex {
display: inline-flex;
}
.size-\[14px\] {
width: 14px;
height: 14px;
}
.h-9 {
height: calc(var(--spacing) * 9);
}
.shrink-0 {
flex-shrink: 0;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.rounded-\[9px\] {
border-radius: 9px;
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-stone-200 {
border-color: var(--color-stone-200);
}
.bg-white {
background-color: var(--color-white);
}
.px-3 {
padding-inline: calc(var(--spacing) * 3);
}
.text-\[13px\] {
font-size: 13px;
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-stone-700 {
color: var(--color-stone-700);
}
.shadow-\[inset_0_1\.5px_0_rgba\(255\,255\,255\,0\.95\)\,inset_0_-1px_0_rgba\(28\,25\,23\,0\.06\)\,0_1px_2px_rgba\(28\,25\,23\,0\.08\)\] {
--tw-shadow: inset 0 1.5px 0 var(--tw-shadow-color, rgba(255,255,255,0.95)), inset 0 -1px 0 var(--tw-shadow-color, rgba(28,25,23,0.06)), 0 1px 2px var(--tw-shadow-color, rgba(28,25,23,0.08));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.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-\[border-color\,box-shadow\,background-color\] {
transition-property: border-color,box-shadow,background-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;
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
@media (hover: hover) {
.hover\:bg-stone-50:hover {
background-color: var(--color-stone-50);
}
}
.focus-visible\:border-\[\#4568FF\]:focus-visible {
border-color: #4568FF;
}
.focus-visible\:shadow-\[0_1px_2px_rgba\(28\,25\,23\,0\.08\)\,0_10px_20px_-14px_rgba\(69\,104\,255\,0\.6\)\]:focus-visible {
--tw-shadow: 0 1px 2px var(--tw-shadow-color, rgba(28,25,23,0.08)), 0 10px 20px -14px var(--tw-shadow-color, rgba(69,104,255,0.6));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.disabled\:opacity-50:disabled {
opacity: 50%;
}
.dark\:border-white\/\[0\.16\]:where(.dark, .dark *) {
border-color: color-mix(in srgb, #fff 16%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-white) 16%, transparent);
}
}
.dark\:bg-\[\#252522\]:where(.dark, .dark *) {
background-color: #252522;
}
.dark\:text-stone-200:where(.dark, .dark *) {
color: var(--color-stone-200);
}
.dark\:shadow-\[inset_0_1px_0_rgba\(255\,255\,255\,0\.07\)\,0_1px_2px_rgba\(0\,0\,0\,0\.4\)\]:where(.dark, .dark *) {
--tw-shadow: inset 0 1px 0 var(--tw-shadow-color, rgba(255,255,255,0.07)), 0 1px 2px var(--tw-shadow-color, rgba(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);
}
@media (hover: hover) {
.dark\:hover\:bg-\[\#2A2A27\]:where(.dark, .dark *):hover {
background-color: #2A2A27;
}
}
.dark\:focus-visible\:border-\[\#93B0FF\]:where(.dark, .dark *):focus-visible {
border-color: #93B0FF;
}
.dark\:focus-visible\:shadow-\[0_10px_20px_-14px_rgba\(147\,176\,255\,0\.5\)\]:where(.dark, .dark *):focus-visible {
--tw-shadow: 0 10px 20px -14px var(--tw-shadow-color, rgba(147,176,255,0.5));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
: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 {
--bezel: #ffffff;
--panel: #ffffff;
--well: #ffffff;
--sub: #fafafa;
--hairline: rgba(0, 0, 0, 0.14);
--hairline-strong: rgba(0, 0, 0, 0.26);
--ink: #131313;
--ink-2: #454545;
--ink-3: #6e6e6e;
--accent: #4568ff;
--accent-soft: #edf1ff;
--moss: #3d7a4e;
--flag: #c0442f;
--shadow-float: 0 28px 56px -24px rgba(0, 0, 0, 0.22),
0 0 0 1px rgba(0, 0, 0, 0.06);
--shadow-lift: 0 1px 2px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(0, 0, 0, 0.11);
--shadow-well: inset 0 1px 2px rgba(0, 0, 0, 0.05),
inset 0 0 0 1px rgba(0, 0, 0, 0.1);
--shadow-cap: 0 1.5px 0 rgba(0, 0, 0, 0.09),
inset 0 0 0 1px rgba(0, 0, 0, 0.11);
--shadow-row: inset 0 0 0 1px rgba(0, 0, 0, 0.07),
0 1px 2px rgba(0, 0, 0, 0.05);
}
.dark {
--bezel: #141312;
--panel: #1d1d1a;
--well: #252522;
--sub: #1a1a17;
--hairline: rgba(255, 255, 255, 0.16);
--hairline-strong: rgba(255, 255, 255, 0.28);
--ink: #f8f8f4;
--ink-2: #c2c2ba;
--ink-3: #93938b;
--accent: #93b0ff;
--accent-soft: rgba(147, 176, 255, 0.14);
--moss: #5bd79c;
--flag: #f5897f;
--shadow-float: 0 28px 56px -22px rgba(0, 0, 0, 0.7),
0 0 0 1px rgba(255, 255, 255, 0.05);
--shadow-lift: 0 1px 2px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 0 0 1px rgba(255, 255, 255, 0.05);
--shadow-well: inset 0 1px 3px rgba(0, 0, 0, 0.4),
inset 0 0 0 1px rgba(255, 255, 255, 0.03);
--shadow-cap: 0 1.5px 0 rgba(0, 0, 0, 0.5),
inset 0 0 0 1px rgba(255, 255, 255, 0.1);
--shadow-row: inset 0 0 0 1px rgba(255, 255, 255, 0.06),
0 1px 2px rgba(0, 0, 0, 0.25);
}
@keyframes reaction-drift {
to {
transform: translateX(-50%);
}
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@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-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;
}
@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-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-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;
}
}
}