"use client"
import { CSSProperties, useEffect, useRef, useState } from "react"
import {Check,ChevronRight} from "lucide-react";
import { cn } from "@/lib/utils"
export interface SwipeButtonProps extends React.HTMLAttributes<HTMLDivElement> {
onSwipeComplete?: () => void
text?: string
className?: string
gap?: number
validationDuration?: number
}
export function SwipeButton({
onSwipeComplete,
text = "Swipe to validate",
className,
gap = 3,
validationDuration = 2000,
...props
}: SwipeButtonProps) {
const [isSwiped, setIsSwiped] = useState(false)
const [isValidated, setIsValidated] = useState(false)
const [startX, setStartX] = useState(0)
const [currentX, setCurrentX] = useState(0)
const [isDragging, setIsDragging] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (isValidated) {
const timer = setTimeout(() => {
setIsValidated(false)
setIsSwiped(false)
setCurrentX(0)
setIsDragging(false)
}, validationDuration)
return () => clearTimeout(timer)
}
}, [isValidated, validationDuration])
const handleStart = (clientX: number) => {
if (isValidated) return
setStartX(clientX)
setIsDragging(true)
}
const handleMove = (clientX: number) => {
if (!buttonRef.current || !isDragging || isValidated) return
const containerWidth = containerRef.current?.offsetWidth || 0
const buttonWidth = buttonRef.current.offsetWidth
const maxSwipe = containerWidth - buttonWidth - gap * 2
let newX = clientX - startX
newX = Math.max(0, Math.min(newX, maxSwipe))
setCurrentX(newX)
setIsSwiped(newX >= maxSwipe - 10)
}
const handleEnd = () => {
if (isValidated) return
if (isSwiped) {
setIsValidated(true)
setCurrentX(0)
onSwipeComplete?.()
} else {
setCurrentX(0)
setIsSwiped(false)
}
setIsDragging(false)
}
return (
<div
ref={containerRef}
className={cn(
"relative h-10 w-[250px] overflow-hidden rounded-lg",
"border border-neutral-200 bg-white shadow-sm dark:border-neutral-800 dark:bg-neutral-900",
"transition-colors duration-200",
className
)}
onTouchStart={(e) => handleStart(e.touches[0].clientX)}
onTouchMove={(e) => handleMove(e.touches[0].clientX)}
onTouchEnd={handleEnd}
onMouseDown={(e) => handleStart(e.clientX)}
onMouseMove={(e) => handleMove(e.clientX)}
onMouseUp={handleEnd}
onMouseLeave={handleEnd}
role="button"
aria-label="Swipe to validate"
{...props}
>
<button
ref={buttonRef}
className={cn(
"absolute rounded-md",
"bg-neutral-900 text-white dark:bg-white dark:text-neutral-900",
"flex items-center justify-center",
"cursor-grab active:cursor-grabbing",
"shadow-sm transition-all duration-300",
"hover:bg-neutral-800 dark:hover:bg-neutral-100",
"focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:outline-none dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900",
"disabled:pointer-events-none",
isValidated &&
"w-[calc(100%-6px)] cursor-default bg-emerald-500 opacity-100 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-500"
)}
style={{
width: isValidated ? `calc(100% - ${gap * 2}px)` : "36px",
height: `calc(100% - ${gap * 2}px)`,
left: isValidated ? `${gap}px` : `${gap}px`,
top: `${gap}px`,
transform: isValidated ? "none" : `translateX(${currentX}px)`,
transition: isDragging ? "none" : "all 0.3s ease",
}}
aria-label={isValidated ? "Validated" : "Swipe to validate"}
disabled={isValidated}
>
{isValidated ? (
<Check className="h-4 w-4" aria-hidden="true" />
) : (
<ChevronRight className="h-4 w-4" aria-hidden="true" />
)}
</button>
<div className="flex h-full w-full items-center justify-center">
<span
style={{ "--swipe-button-text-width": "130px" } as CSSProperties}
className={cn(
"pointer-events-none mx-auto max-w-md text-sm text-neutral-600/70 dark:text-neutral-400/70",
"animate-swipe-button-text [background-size:var(--swipe-button-text-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat select-none [transition:background-position_1s_cubic-bezier(.4,0,.2,1)_infinite]",
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80"
)}
>
{text}
</span>
</div>
</div>
)
}
Swipe Button
By badtz
Try the interaction, copy the React source files, or download the complete example.
About Swipe Button
Swipe Button is an animated React and Tailwind CSS button by badtz. 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
registry/new-york-v4/examples/swipe-button-demo.tsx
"use client"
import confetti from "canvas-confetti"
import { SwipeButton } from "@/registry/new-york-v4/ui/swipe-button"
export default function SwipeButtonDemo() {
const triggerConfetti = () => {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
})
}
return (
<div>
<SwipeButton onSwipeComplete={triggerConfetti} />
</div>
)
}
registry/new-york-v4/ui/swipe-button.tsx
"use client"
import { CSSProperties, useEffect, useRef, useState } from "react"
import {Check,ChevronRight} from "lucide-react";
import { cn } from "@/lib/utils"
export interface SwipeButtonProps extends React.HTMLAttributes<HTMLDivElement> {
onSwipeComplete?: () => void
text?: string
className?: string
gap?: number
validationDuration?: number
}
export function SwipeButton({
onSwipeComplete,
text = "Swipe to validate",
className,
gap = 3,
validationDuration = 2000,
...props
}: SwipeButtonProps) {
const [isSwiped, setIsSwiped] = useState(false)
const [isValidated, setIsValidated] = useState(false)
const [startX, setStartX] = useState(0)
const [currentX, setCurrentX] = useState(0)
const [isDragging, setIsDragging] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (isValidated) {
const timer = setTimeout(() => {
setIsValidated(false)
setIsSwiped(false)
setCurrentX(0)
setIsDragging(false)
}, validationDuration)
return () => clearTimeout(timer)
}
}, [isValidated, validationDuration])
const handleStart = (clientX: number) => {
if (isValidated) return
setStartX(clientX)
setIsDragging(true)
}
const handleMove = (clientX: number) => {
if (!buttonRef.current || !isDragging || isValidated) return
const containerWidth = containerRef.current?.offsetWidth || 0
const buttonWidth = buttonRef.current.offsetWidth
const maxSwipe = containerWidth - buttonWidth - gap * 2
let newX = clientX - startX
newX = Math.max(0, Math.min(newX, maxSwipe))
setCurrentX(newX)
setIsSwiped(newX >= maxSwipe - 10)
}
const handleEnd = () => {
if (isValidated) return
if (isSwiped) {
setIsValidated(true)
setCurrentX(0)
onSwipeComplete?.()
} else {
setCurrentX(0)
setIsSwiped(false)
}
setIsDragging(false)
}
return (
<div
ref={containerRef}
className={cn(
"relative h-10 w-[250px] overflow-hidden rounded-lg",
"border border-neutral-200 bg-white shadow-sm dark:border-neutral-800 dark:bg-neutral-900",
"transition-colors duration-200",
className
)}
onTouchStart={(e) => handleStart(e.touches[0].clientX)}
onTouchMove={(e) => handleMove(e.touches[0].clientX)}
onTouchEnd={handleEnd}
onMouseDown={(e) => handleStart(e.clientX)}
onMouseMove={(e) => handleMove(e.clientX)}
onMouseUp={handleEnd}
onMouseLeave={handleEnd}
role="button"
aria-label="Swipe to validate"
{...props}
>
<button
ref={buttonRef}
className={cn(
"absolute rounded-md",
"bg-neutral-900 text-white dark:bg-white dark:text-neutral-900",
"flex items-center justify-center",
"cursor-grab active:cursor-grabbing",
"shadow-sm transition-all duration-300",
"hover:bg-neutral-800 dark:hover:bg-neutral-100",
"focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:outline-none dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900",
"disabled:pointer-events-none",
isValidated &&
"w-[calc(100%-6px)] cursor-default bg-emerald-500 opacity-100 hover:bg-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-500"
)}
style={{
width: isValidated ? `calc(100% - ${gap * 2}px)` : "36px",
height: `calc(100% - ${gap * 2}px)`,
left: isValidated ? `${gap}px` : `${gap}px`,
top: `${gap}px`,
transform: isValidated ? "none" : `translateX(${currentX}px)`,
transition: isDragging ? "none" : "all 0.3s ease",
}}
aria-label={isValidated ? "Validated" : "Swipe to validate"}
disabled={isValidated}
>
{isValidated ? (
<Check className="h-4 w-4" aria-hidden="true" />
) : (
<ChevronRight className="h-4 w-4" aria-hidden="true" />
)}
</button>
<div className="flex h-full w-full items-center justify-center">
<span
style={{ "--swipe-button-text-width": "130px" } as CSSProperties}
className={cn(
"pointer-events-none mx-auto max-w-md text-sm text-neutral-600/70 dark:text-neutral-400/70",
"animate-swipe-button-text [background-size:var(--swipe-button-text-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat select-none [transition:background-position_1s_cubic-bezier(.4,0,.2,1)_infinite]",
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80"
)}
>
{text}
</span>
</div>
</div>
)
}
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function absoluteUrl(path: string) {
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
}
demo.tsx
export { default as default } from "@/registry/new-york-v4/examples/swipe-button-demo";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--color-emerald-500: oklch(69.6% 0.17 162.48);
--color-blue-300: oklch(80.9% 0.105 251.813);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-blue-800: oklch(42.4% 0.199 265.638);
--color-neutral-100: oklch(97% 0 none);
--color-neutral-200: oklch(92.2% 0 none);
--color-neutral-400: oklch(70.8% 0 none);
--color-neutral-600: oklch(43.9% 0 none);
--color-neutral-800: oklch(26.9% 0 none);
--color-neutral-900: oklch(20.5% 0 none);
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
--container-md: 28rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--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);
--font-instrument-serif: var(--font-instrument-serif);
}
}
@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;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.mx-auto {
margin-inline: auto;
}
.flex {
display: flex;
}
.h-4 {
height: calc(var(--spacing) * 4);
}
.h-10 {
height: calc(var(--spacing) * 10);
}
.h-full {
height: 100%;
}
.w-4 {
width: calc(var(--spacing) * 4);
}
.w-\[250px\] {
width: 250px;
}
.w-\[calc\(100\%-6px\)\] {
width: calc(100% - 6px);
}
.w-full {
width: 100%;
}
.max-w-md {
max-width: var(--container-md);
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.animate-swipe-button-text {
animation: swipe-button-text 1.5s
cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.cursor-default {
cursor: default;
}
.cursor-grab {
cursor: grab;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.overflow-hidden {
overflow: hidden;
}
.rounded-lg {
border-radius: var(--radius);
}
.rounded-md {
border-radius: calc(var(--radius) - 2px);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-neutral-200 {
border-color: var(--color-neutral-200);
}
.bg-emerald-500 {
background-color: var(--color-emerald-500);
}
.bg-neutral-900 {
background-color: var(--color-neutral-900);
}
.bg-white {
background-color: var(--color-white);
}
.bg-gradient-to-r {
--tw-gradient-position: to right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-transparent {
--tw-gradient-from: transparent;
--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));
}
.via-black\/80 {
--tw-gradient-via: color-mix(in srgb, #000 80%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-gradient-via: color-mix(in oklab, var(--color-black) 80%, transparent);
}
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-50\% {
--tw-gradient-via-position: 50%;
}
.to-transparent {
--tw-gradient-to: transparent;
--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));
}
.\[background-size\:var\(--swipe-button-text-width\)_100\%\] {
background-size: var(--swipe-button-text-width) 100%;
}
.bg-clip-text {
background-clip: text;
}
.\[background-position\:0_0\] {
background-position: 0 0;
}
.bg-no-repeat {
background-repeat: no-repeat;
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.text-neutral-600\/70 {
color: color-mix(in srgb, oklch(43.9% 0 none) 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-neutral-600) 70%, transparent);
}
}
.text-white {
color: var(--color-white);
}
.opacity-100 {
opacity: 100%;
}
.shadow-sm {
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
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-all {
transition-property: all;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-colors {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-200 {
--tw-duration: 200ms;
transition-duration: 200ms;
}
.duration-300 {
--tw-duration: 300ms;
transition-duration: 300ms;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.\[transition\:background-position_1s_cubic-bezier\(\.4\,0\,\.2\,1\)_infinite\] {
transition: background-position 1s cubic-bezier(.4,0,.2,1) infinite;
}
@media (hover: hover) {
.hover\:bg-emerald-500:hover {
background-color: var(--color-emerald-500);
}
.hover\:bg-neutral-800:hover {
background-color: var(--color-neutral-800);
}
}
.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-neutral-400:focus-visible {
--tw-ring-color: var(--color-neutral-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);
}
.focus-visible\:outline-none:focus-visible {
--tw-outline-style: none;
outline-style: none;
}
.active\:cursor-grabbing:active {
cursor: grabbing;
}
.disabled\:pointer-events-none:disabled {
pointer-events: none;
}
.dark\:border-neutral-800:where(.dark, .dark *) {
border-color: var(--color-neutral-800);
}
.dark\:bg-emerald-500:where(.dark, .dark *) {
background-color: var(--color-emerald-500);
}
.dark\:bg-neutral-900:where(.dark, .dark *) {
background-color: var(--color-neutral-900);
}
.dark\:bg-white:where(.dark, .dark *) {
background-color: var(--color-white);
}
.dark\:via-white\/80:where(.dark, .dark *) {
--tw-gradient-via: color-mix(in srgb, #fff 80%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-gradient-via: color-mix(in oklab, var(--color-white) 80%, transparent);
}
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.dark\:text-neutral-400\/70:where(.dark, .dark *) {
color: color-mix(in srgb, oklch(70.8% 0 none) 70%, transparent);
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--color-neutral-400) 70%, transparent);
}
}
.dark\:text-neutral-900:where(.dark, .dark *) {
color: var(--color-neutral-900);
}
@media (hover: hover) {
.dark\:hover\:bg-emerald-500:where(.dark, .dark *):hover {
background-color: var(--color-emerald-500);
}
.dark\:hover\:bg-neutral-100:where(.dark, .dark *):hover {
background-color: var(--color-neutral-100);
}
}
.dark\:focus-visible\:ring-neutral-600:where(.dark, .dark *):focus-visible {
--tw-ring-color: var(--color-neutral-600);
}
.dark\:focus-visible\:ring-offset-neutral-900:where(.dark, .dark *):focus-visible {
--tw-ring-offset-color: var(--color-neutral-900);
}
}
: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 {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.6049 0.1419 276.7);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.832 0.1638 83.82);
--accent-foreground: oklch(0.205 0 0);
--accent-border: oklch(0.602 0.1186 283.39);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.99 0 0);
--sidebar-foreground: oklch(1 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);
--surface: oklch(0.99 0 0);
--surface-foreground: var(--foreground);
--code: var(--surface);
--code-foreground: var(--surface-foreground);
--code-highlight: oklch(0.96 0 0);
--code-number: oklch(0.56 0 0);
--selection: oklch(0.832 0.1638 83.82);
--selection-foreground: oklch(1 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.269 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.6049 0.1419 276.7);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.1826 0.002 286.2);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.5601 0.1884 258.36);
--accent-foreground: oklch(0.985 0 0);
--accent-border: oklch(0.602 0.1186 283.39);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.1826 0.002 286.2);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 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.439 0 0);
--surface: oklch(0.1826 0.002 286.2);
--surface-foreground: oklch(0.708 0 0);
--code: var(--surface);
--code-foreground: var(--surface-foreground);
--code-highlight: oklch(0.27 0 0);
--code-number: oklch(0.72 0 0);
--selection: oklch(0.832 0.1638 83.82);
--selection-foreground: oklch(1 0 0);
}
@keyframes infinite-ribbon {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
@keyframes infinite-ribbon-reverse {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0);
}
}
@keyframes marquee-left {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(-100% - var(--gap)));
}
}
@keyframes marquee-right {
from {
transform: translateX(calc(-100% - var(--gap)));
}
to {
transform: translateX(0);
}
}
@keyframes meteor {
0% {
transform: rotate(170deg) translateX(0);
opacity: 1;
}
70% {
opacity: 1;
}
100% {
transform: rotate(170deg) translateX(-500px);
opacity: 0;
}
}
@keyframes border-beam-flow {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
@keyframes border-beam-rotate {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes svg-line-flow {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(33.33%);
}
}
@keyframes flow-gradient {
0% {
background-position: -100% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes swipe-button-text {
0% {
background-position: 0 0;
}
100% {
background-position: var(--swipe-button-text-width) 0;
}
}
@keyframes star-btn {
0% {
offset-distance: 0%;
}
100% {
offset-distance: 100%;
}
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@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-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;
}
@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-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-border-style: solid;
--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-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;
}
}
}