import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/packages/m3e/src/lib/cn"
import { Ripple } from "@/packages/m3e/src/components/ripple"
/*
* M3 Expressive button.
*
* Icon sizes (icon-*) also take width="narrow" | "wide" (M3E icon button widths).
*
* Each size sets three corner radii as CSS variables:
* --btn-r resting corner (round = half the height, square = size-specific)
* --btn-r-pressed corner while pressed (the M3E shape morph)
* --btn-r-selected corner of a selected toggle (round ↔ square swap)
*/
const buttonVariants = cva(
"group/button state-layer relative inline-flex shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-(--btn-r) border border-transparent whitespace-nowrap focus-ring transition-shape outline-none select-none disabled:pointer-events-none disabled:cursor-default disabled:border-transparent! disabled:bg-on-surface/10! disabled:text-on-surface/38! disabled:shadow-none! aria-invalid:border-error data-press:rounded-(--btn-r-pressed) [&_svg]:pointer-events-none [&_svg]:shrink-0 [&>*:not([data-slot=ripple])]:relative [&>*:not([data-slot=ripple])]:z-1",
{
variants: {
variant: {
/* M3 names */
filled:
"bg-primary text-on-primary hover:shadow-elevation-1 data-press:shadow-none",
tonal:
"bg-secondary-container text-on-secondary-container hover:shadow-elevation-1 data-press:shadow-none",
elevated:
"bg-surface-container-low text-primary shadow-elevation-1 hover:shadow-elevation-2 data-press:shadow-elevation-1",
outlined:
"border-outline-variant bg-transparent text-on-surface-variant disabled:border-on-surface/12!",
text: "bg-transparent text-primary disabled:bg-transparent!",
/* shadcn names */
default:
"bg-primary text-on-primary hover:shadow-elevation-1 data-press:shadow-none",
secondary:
"bg-secondary-container text-on-secondary-container hover:shadow-elevation-1 data-press:shadow-none",
tertiary:
"bg-tertiary-container text-on-tertiary-container hover:shadow-elevation-1 data-press:shadow-none",
outline:
"border-outline-variant bg-transparent text-on-surface-variant disabled:border-on-surface/12! aria-expanded:bg-on-surface-variant/8",
ghost:
"bg-transparent text-on-surface-variant disabled:bg-transparent! aria-expanded:bg-on-surface-variant/8",
destructive:
"bg-error text-on-error hover:shadow-elevation-1 data-press:shadow-none",
link: "state-layer-transparent bg-transparent text-primary underline-offset-4 hover:underline disabled:bg-transparent!",
},
size: {
xs: "h-8 gap-2 px-4 text-label-large [--btn-h2:16px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:16px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg:not([class*='size-'])]:size-5",
sm: "h-10 gap-2 px-4 text-label-large [--btn-h2:20px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:20px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg:not([class*='size-'])]:size-5",
default:
"h-10 gap-2 px-4 text-label-large [--btn-h2:20px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:20px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg:not([class*='size-'])]:size-5",
md: "h-14 gap-2 px-6 text-title-medium [--btn-h2:28px] [--btn-r-pressed:var(--radius-md)] [--btn-r-square:var(--radius-lg)] [--btn-r:28px] has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5 [&_svg:not([class*='size-'])]:size-6",
lg: "h-24 gap-3 px-12 text-headline-small [--btn-h2:48px] [--btn-r-pressed:var(--radius-lg)] [--btn-r-square:var(--radius-2xl)] [--btn-r:48px] data-[variant=outline]:border-2 data-[variant=outlined]:border-2 [&_svg:not([class*='size-'])]:size-8",
xl: "h-34 gap-4 px-16 text-headline-large [--btn-h2:68px] [--btn-r-pressed:var(--radius-lg)] [--btn-r-square:var(--radius-2xl)] [--btn-r:68px] data-[variant=outline]:border-3 data-[variant=outlined]:border-3 [&_svg:not([class*='size-'])]:size-10",
/* icon buttons */
"icon-xs":
"size-8 [--btn-h2:16px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:16px] [&_svg:not([class*='size-'])]:size-5",
"icon-sm":
"size-10 [--btn-h2:20px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:20px] [&_svg:not([class*='size-'])]:size-6",
icon: "size-10 [--btn-h2:20px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:20px] [&_svg:not([class*='size-'])]:size-6",
"icon-md":
"size-14 [--btn-h2:28px] [--btn-r-pressed:var(--radius-md)] [--btn-r-square:var(--radius-lg)] [--btn-r:28px] [&_svg:not([class*='size-'])]:size-6",
"icon-lg":
"size-24 [--btn-h2:48px] [--btn-r-pressed:var(--radius-lg)] [--btn-r-square:var(--radius-2xl)] [--btn-r:48px] [&_svg:not([class*='size-'])]:size-8",
"icon-xl":
"size-34 [--btn-h2:68px] [--btn-r-pressed:var(--radius-lg)] [--btn-r-square:var(--radius-2xl)] [--btn-r:68px] [&_svg:not([class*='size-'])]:size-10",
},
shape: {
round: "",
square: "[--btn-r:var(--btn-r-square)]",
},
/* icon buttons: M3E offers three widths at every size */
width: {
default: "",
narrow: "",
wide: "",
},
},
compoundVariants: [
{ size: "icon-xs", width: "narrow", className: "w-[28px]!" },
{ size: "icon-xs", width: "wide", className: "w-[40px]!" },
{ size: "icon-sm", width: "narrow", className: "w-[32px]!" },
{ size: "icon-sm", width: "wide", className: "w-[52px]!" },
{ size: "icon", width: "narrow", className: "w-[32px]!" },
{ size: "icon", width: "wide", className: "w-[52px]!" },
{ size: "icon-md", width: "narrow", className: "w-[48px]!" },
{ size: "icon-md", width: "wide", className: "w-[72px]!" },
{ size: "icon-lg", width: "narrow", className: "w-[64px]!" },
{ size: "icon-lg", width: "wide", className: "w-[128px]!" },
{ size: "icon-xl", width: "narrow", className: "w-[104px]!" },
{ size: "icon-xl", width: "wide", className: "w-[184px]!" },
],
defaultVariants: {
variant: "default",
size: "default",
shape: "round",
width: "default",
},
}
)
type ButtonProps = ButtonPrimitive.Props &
VariantProps<typeof buttonVariants> & {
disableRipple?: boolean
}
function Button({
className,
variant = "default",
size = "default",
shape = "round",
width = "default",
disableRipple,
children,
...props
}: ButtonProps) {
return (
<ButtonPrimitive
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, shape, width, className }))}
{...props}
>
<Ripple disabled={disableRipple || variant === "link"} />
{children}
</ButtonPrimitive>
)
}
export { Button, buttonVariants }
export type { ButtonProps }
01 Variants
By crysta1221
Try the interaction, copy the React source files, or download the complete example.
About 01 Variants
01 Variants is an animated React and Tailwind CSS button by crysta1221. 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
apps/docs/src/docs/examples/button/01-variants.tsx
import { Button } from "@/packages/m3e/src/components/button"
export const meta = {
title: "Variants",
description: "Filled is the strongest, text the quietest.",
}
export default function Demo() {
return (
<>
<Button variant="filled">Filled</Button>
<Button variant="tonal">Tonal</Button>
<Button variant="elevated">Elevated</Button>
<Button variant="outlined">Outlined</Button>
<Button variant="text">Text</Button>
<Button disabled>Disabled</Button>
</>
)
}
packages/m3e/src/components/button.tsx
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/packages/m3e/src/lib/cn"
import { Ripple } from "@/packages/m3e/src/components/ripple"
/*
* M3 Expressive button.
*
* Icon sizes (icon-*) also take width="narrow" | "wide" (M3E icon button widths).
*
* Each size sets three corner radii as CSS variables:
* --btn-r resting corner (round = half the height, square = size-specific)
* --btn-r-pressed corner while pressed (the M3E shape morph)
* --btn-r-selected corner of a selected toggle (round ↔ square swap)
*/
const buttonVariants = cva(
"group/button state-layer relative inline-flex shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-(--btn-r) border border-transparent whitespace-nowrap focus-ring transition-shape outline-none select-none disabled:pointer-events-none disabled:cursor-default disabled:border-transparent! disabled:bg-on-surface/10! disabled:text-on-surface/38! disabled:shadow-none! aria-invalid:border-error data-press:rounded-(--btn-r-pressed) [&_svg]:pointer-events-none [&_svg]:shrink-0 [&>*:not([data-slot=ripple])]:relative [&>*:not([data-slot=ripple])]:z-1",
{
variants: {
variant: {
/* M3 names */
filled:
"bg-primary text-on-primary hover:shadow-elevation-1 data-press:shadow-none",
tonal:
"bg-secondary-container text-on-secondary-container hover:shadow-elevation-1 data-press:shadow-none",
elevated:
"bg-surface-container-low text-primary shadow-elevation-1 hover:shadow-elevation-2 data-press:shadow-elevation-1",
outlined:
"border-outline-variant bg-transparent text-on-surface-variant disabled:border-on-surface/12!",
text: "bg-transparent text-primary disabled:bg-transparent!",
/* shadcn names */
default:
"bg-primary text-on-primary hover:shadow-elevation-1 data-press:shadow-none",
secondary:
"bg-secondary-container text-on-secondary-container hover:shadow-elevation-1 data-press:shadow-none",
tertiary:
"bg-tertiary-container text-on-tertiary-container hover:shadow-elevation-1 data-press:shadow-none",
outline:
"border-outline-variant bg-transparent text-on-surface-variant disabled:border-on-surface/12! aria-expanded:bg-on-surface-variant/8",
ghost:
"bg-transparent text-on-surface-variant disabled:bg-transparent! aria-expanded:bg-on-surface-variant/8",
destructive:
"bg-error text-on-error hover:shadow-elevation-1 data-press:shadow-none",
link: "state-layer-transparent bg-transparent text-primary underline-offset-4 hover:underline disabled:bg-transparent!",
},
size: {
xs: "h-8 gap-2 px-4 text-label-large [--btn-h2:16px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:16px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg:not([class*='size-'])]:size-5",
sm: "h-10 gap-2 px-4 text-label-large [--btn-h2:20px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:20px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg:not([class*='size-'])]:size-5",
default:
"h-10 gap-2 px-4 text-label-large [--btn-h2:20px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:20px] has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg:not([class*='size-'])]:size-5",
md: "h-14 gap-2 px-6 text-title-medium [--btn-h2:28px] [--btn-r-pressed:var(--radius-md)] [--btn-r-square:var(--radius-lg)] [--btn-r:28px] has-data-[icon=inline-end]:pr-5 has-data-[icon=inline-start]:pl-5 [&_svg:not([class*='size-'])]:size-6",
lg: "h-24 gap-3 px-12 text-headline-small [--btn-h2:48px] [--btn-r-pressed:var(--radius-lg)] [--btn-r-square:var(--radius-2xl)] [--btn-r:48px] data-[variant=outline]:border-2 data-[variant=outlined]:border-2 [&_svg:not([class*='size-'])]:size-8",
xl: "h-34 gap-4 px-16 text-headline-large [--btn-h2:68px] [--btn-r-pressed:var(--radius-lg)] [--btn-r-square:var(--radius-2xl)] [--btn-r:68px] data-[variant=outline]:border-3 data-[variant=outlined]:border-3 [&_svg:not([class*='size-'])]:size-10",
/* icon buttons */
"icon-xs":
"size-8 [--btn-h2:16px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:16px] [&_svg:not([class*='size-'])]:size-5",
"icon-sm":
"size-10 [--btn-h2:20px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:20px] [&_svg:not([class*='size-'])]:size-6",
icon: "size-10 [--btn-h2:20px] [--btn-r-pressed:var(--radius-sm)] [--btn-r-square:var(--radius-md)] [--btn-r:20px] [&_svg:not([class*='size-'])]:size-6",
"icon-md":
"size-14 [--btn-h2:28px] [--btn-r-pressed:var(--radius-md)] [--btn-r-square:var(--radius-lg)] [--btn-r:28px] [&_svg:not([class*='size-'])]:size-6",
"icon-lg":
"size-24 [--btn-h2:48px] [--btn-r-pressed:var(--radius-lg)] [--btn-r-square:var(--radius-2xl)] [--btn-r:48px] [&_svg:not([class*='size-'])]:size-8",
"icon-xl":
"size-34 [--btn-h2:68px] [--btn-r-pressed:var(--radius-lg)] [--btn-r-square:var(--radius-2xl)] [--btn-r:68px] [&_svg:not([class*='size-'])]:size-10",
},
shape: {
round: "",
square: "[--btn-r:var(--btn-r-square)]",
},
/* icon buttons: M3E offers three widths at every size */
width: {
default: "",
narrow: "",
wide: "",
},
},
compoundVariants: [
{ size: "icon-xs", width: "narrow", className: "w-[28px]!" },
{ size: "icon-xs", width: "wide", className: "w-[40px]!" },
{ size: "icon-sm", width: "narrow", className: "w-[32px]!" },
{ size: "icon-sm", width: "wide", className: "w-[52px]!" },
{ size: "icon", width: "narrow", className: "w-[32px]!" },
{ size: "icon", width: "wide", className: "w-[52px]!" },
{ size: "icon-md", width: "narrow", className: "w-[48px]!" },
{ size: "icon-md", width: "wide", className: "w-[72px]!" },
{ size: "icon-lg", width: "narrow", className: "w-[64px]!" },
{ size: "icon-lg", width: "wide", className: "w-[128px]!" },
{ size: "icon-xl", width: "narrow", className: "w-[104px]!" },
{ size: "icon-xl", width: "wide", className: "w-[184px]!" },
],
defaultVariants: {
variant: "default",
size: "default",
shape: "round",
width: "default",
},
}
)
type ButtonProps = ButtonPrimitive.Props &
VariantProps<typeof buttonVariants> & {
disableRipple?: boolean
}
function Button({
className,
variant = "default",
size = "default",
shape = "round",
width = "default",
disableRipple,
children,
...props
}: ButtonProps) {
return (
<ButtonPrimitive
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, shape, width, className }))}
{...props}
>
<Ripple disabled={disableRipple || variant === "link"} />
{children}
</ButtonPrimitive>
)
}
export { Button, buttonVariants }
export type { ButtonProps }
packages/m3e/src/lib/cn.ts
/**
* `cn` taught the M3 Expressive utilities, so it doesn't mistake
* `text-label-large` (a font size) for a text color and drop `text-on-primary`.
*
* M3E components import it from here. Bare `import { cn } from "cn"` (what
* `shadcn add` writes) is aliased to this module in vite.config.ts and
* tsconfig, so components added later pick it up too.
*/
import { createCn } from "cn/config"
const TYPE_ROLES = ["display", "headline", "title", "body", "label"]
const TYPE_SIZES = ["large", "medium", "small"]
const typeScale = TYPE_ROLES.flatMap((r) =>
TYPE_SIZES.flatMap((s) => [`${r}-${s}`, `${r}-${s}-emphasized`])
)
export const cn = createCn({
extend: {
classGroups: {
"font-size": [{ text: typeScale }],
shadow: [
{
shadow: [
"elevation-0",
"elevation-1",
"elevation-2",
"elevation-3",
"elevation-4",
"elevation-5",
],
},
],
"state-layer": ["state-layer", "state-layer-circle"],
"state-layer-color": [{ "state-layer": [(v: string) => v !== "circle"] }],
"ripple-color": [{ ripple: [() => true] }],
motion: [
{
motion: [
"spatial-fast",
"spatial-default",
"spatial-slow",
"effects-fast",
"effects-default",
"effects-slow",
],
},
],
},
},
})
// "cn/engine" is not aliased, so this does not import this module back
export { clsx, twJoin } from "cn/engine"
packages/m3e/src/components/ripple.tsx
import * as React from "react"
import { cn } from "@/packages/m3e/src/lib/cn"
/*
* Press feedback for M3 components.
*
* 1. Pressed state — `data-press` on the control, held for at least 225ms so a
* quick click still shows the whole shape morph (matraic/m3e
* PressedController: pointer presses start at once, Space holds until
* keyup, Enter/other keyboard clicks hold for the minimum).
*
* 2. Ripple — port of Material Web's <md-ripple> press animation (Apache-2.0,
* material-components/material-web ripple/internal/ripple.ts): a soft-edged
* circle starts at 20% of the larger side on the press and grows past the
* corners while travelling to the centre, 450ms on the standard easing;
* fades in over 105ms, out over 375ms; touches wait 150ms so scrolling
* never lights anything up.
*/
const MINIMUM_PRESS_MS = 225
const PRESS_GROW_MS = 450
const INITIAL_ORIGIN_SCALE = 0.2
const PADDING = 10
const SOFT_EDGE_MINIMUM_SIZE = 75
const SOFT_EDGE_CONTAINER_RATIO = 0.35
const TOUCH_DELAY_MS = 150
const EASING_STANDARD = "cubic-bezier(0.2, 0, 0, 1)"
type RippleProps = {
/** turn the ripple off (the pressed state is still tracked) */
disabled?: boolean
className?: string
}
function isDisabled(el: HTMLElement) {
return (
el.matches(":disabled") ||
el.getAttribute("aria-disabled") === "true" ||
el.hasAttribute("data-disabled")
)
}
/** the control a ripple host belongs to */
function controlOf(host: HTMLElement): HTMLElement | null {
let el = host.parentElement
// inside a non-interactive part (e.g. a switch handle): use the control
if (el && getComputedStyle(el).pointerEvents === "none") {
el =
el.parentElement?.closest<HTMLElement>("button, a, [role], label") ?? null
}
return el
}
/**
* Tracks the pressed state of `target` as `data-press`, held for at least
* 225ms. Returns a cleanup function.
*/
function trackPress(target: HTMLElement) {
let since = 0
let timer = 0
// the click a Space press produces on keyup is part of that press
let spaceClick = false
const down = () => {
window.clearTimeout(timer)
since = performance.now()
target.setAttribute("data-press", "")
}
const release = () => target.removeAttribute("data-press")
const up = () => {
if (!since) return
const left = MINIMUM_PRESS_MS - (performance.now() - since)
since = 0
if (left > 0) timer = window.setTimeout(release, left)
else release()
}
const onPointerDown = (e: PointerEvent) => {
if (e.pointerType === "mouse" && e.button > 1) return
if (isDisabled(target)) return
down()
}
const onKeyDown = (e: KeyboardEvent) => {
if (e.target !== target || e.repeat || isDisabled(target)) return
if (e.key === " ") {
spaceClick = true
down()
}
}
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === " ") up()
}
// Enter (and other synthetic clicks) carry detail 0
const onClick = (e: MouseEvent) => {
if (spaceClick) {
spaceClick = false
return
}
if (e.detail === 0 && !since && !isDisabled(target)) {
down()
up()
}
}
target.addEventListener("pointerdown", onPointerDown)
target.addEventListener("keydown", onKeyDown)
target.addEventListener("keyup", onKeyUp)
target.addEventListener("click", onClick)
document.addEventListener("pointerup", up)
document.addEventListener("pointercancel", up)
return () => {
window.clearTimeout(timer)
target.removeAttribute("data-press")
target.removeEventListener("pointerdown", onPointerDown)
target.removeEventListener("keydown", onKeyDown)
target.removeEventListener("keyup", onKeyUp)
target.removeEventListener("click", onClick)
document.removeEventListener("pointerup", up)
document.removeEventListener("pointercancel", up)
}
}
/** Material Web ripple on `target`, drawn into `press`. Returns a cleanup. */
function trackRipple(
target: HTMLElement,
host: HTMLElement,
press: HTMLElement
) {
let grow: Animation | null = null
let touchTimer = 0
let pendingTouch: PointerEvent | null = null
const start = (e?: PointerEvent) => {
const { width, height, left, top } = host.getBoundingClientRect()
const maxDim = Math.max(width, height)
const softEdge = Math.max(
SOFT_EDGE_CONTAINER_RATIO * maxDim,
SOFT_EDGE_MINIMUM_SIZE
)
const initial = Math.max(1, Math.floor(maxDim * INITIAL_ORIGIN_SCALE))
const maxRadius = Math.hypot(width, height) + PADDING
const scale = (maxRadius + softEdge) / initial
const from = e
? { x: e.clientX - left - initial / 2, y: e.clientY - top - initial / 2 }
: { x: (width - initial) / 2, y: (height - initial) / 2 }
const to = { x: (width - initial) / 2, y: (height - initial) / 2 }
grow?.cancel()
press.style.width = press.style.height = `${initial}px`
press.setAttribute("data-pressed", "")
grow = press.animate(
{
transform: [
`translate(${from.x}px, ${from.y}px) scale(1)`,
`translate(${to.x}px, ${to.y}px) scale(${scale})`,
],
},
{ duration: PRESS_GROW_MS, easing: EASING_STANDARD, fill: "forwards" }
)
}
const end = () => {
const animation = grow
if (!animation) return
const played = Number(animation.currentTime ?? Infinity)
const release = () => {
if (grow === animation) press.removeAttribute("data-pressed")
}
if (played >= MINIMUM_PRESS_MS) release()
else window.setTimeout(release, MINIMUM_PRESS_MS - played)
}
const cleanupUp = () => {
window.removeEventListener("pointerup", onPointerUp)
window.removeEventListener("pointercancel", onPointerCancel)
}
const onPointerDown = (e: PointerEvent) => {
if (!e.isPrimary || e.button !== 0 || isDisabled(target)) return
if (e.pointerType === "touch") {
pendingTouch = e
touchTimer = window.setTimeout(() => {
if (pendingTouch) start(pendingTouch)
pendingTouch = null
}, TOUCH_DELAY_MS)
} else {
start(e)
}
window.addEventListener("pointerup", onPointerUp)
window.addEventListener("pointercancel", onPointerCancel)
}
const onPointerUp = () => {
cleanupUp()
if (pendingTouch) {
// a quick tap: show the ripple anyway
window.clearTimeout(touchTimer)
start(pendingTouch)
pendingTouch = null
}
end()
}
const onPointerCancel = () => {
cleanupUp()
window.clearTimeout(touchTimer)
pendingTouch = null
end()
}
const onPointerLeave = (e: PointerEvent) => {
if (e.pointerType === "mouse" && grow) end()
}
const onKeyDown = (e: KeyboardEvent) => {
if (
e.target === target &&
e.key === " " &&
!e.repeat &&
!isDisabled(target)
)
start()
}
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === " ") end()
}
const onClick = (e: MouseEvent) => {
if (
e.detail === 0 &&
!isDisabled(target) &&
!press.hasAttribute("data-pressed")
) {
start()
end()
}
}
// A control that turns "current" (navigation item, tab) has done its job:
// let the ripple go at once instead of trailing over the new state.
const settle = new MutationObserver(() => {
if (!grow || !press.hasAttribute("data-pressed")) return
if (
!target.hasAttribute("data-active") &&
!target.hasAttribute("aria-current")
)
return
press.style.transitionDuration = "90ms"
press.removeAttribute("data-pressed")
window.setTimeout(
() => press.style.removeProperty("transition-duration"),
120
)
})
settle.observe(target, {
attributes: true,
attributeFilter: ["data-active", "aria-current"],
})
target.addEventListener("pointerdown", onPointerDown)
target.addEventListener("pointerleave", onPointerLeave)
target.addEventListener("keydown", onKeyDown)
target.addEventListener("keyup", onKeyUp)
target.addEventListener("click", onClick)
return () => {
settle.disconnect()
cleanupUp()
window.clearTimeout(touchTimer)
target.removeEventListener("pointerdown", onPointerDown)
target.removeEventListener("pointerleave", onPointerLeave)
target.removeEventListener("keydown", onKeyDown)
target.removeEventListener("keyup", onKeyUp)
target.removeEventListener("click", onClick)
grow?.cancel()
}
}
/**
* M3 press feedback. Drop it inside any interactive element: it marks the
* element with `data-press` while pressed (≥225ms) and draws the ripple,
* clipped to the element's corners. The element needs `position: relative`
* (the `state-layer` utility sets it). The ripple color is `currentColor`, or
* `--ripple-color`.
*/
function Ripple({ disabled, className }: RippleProps) {
const hostRef = React.useRef<HTMLSpanElement>(null)
const pressRef = React.useRef<HTMLSpanElement>(null)
React.useEffect(() => {
const host = hostRef.current
const target = host && controlOf(host)
if (!target) return undefined
return trackPress(target)
}, [])
React.useEffect(() => {
const host = hostRef.current
const press = pressRef.current
const target = host && controlOf(host)
if (!host || !press || !target || disabled) return undefined
return trackRipple(target, host, press)
}, [disabled])
return (
<span
ref={hostRef}
aria-hidden
data-slot="ripple"
className={cn("m3-ripple-host", className)}
>
<span ref={pressRef} className="m3-ripple" />
</span>
)
}
export { Ripple, trackPress }
demo.tsx
export { default as default } from "@/apps/docs/src/docs/examples/button/01-variants";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--spacing: 0.25rem;
--radius-sm: var(--md-sys-shape-corner-small);
--radius-md: var(--md-sys-shape-corner-medium);
--radius-lg: var(--md-sys-shape-corner-large);
--radius-2xl: var(--md-sys-shape-corner-extra-large);
--default-font-family: "Roboto Flex Variable", Roboto, "Noto Sans JP", system-ui, sans-serif;
--default-mono-font-family: "JetBrains Mono Variable", ui-monospace, "Cascadia Code", monospace;
--text-headline-large: 2rem;
--text-headline-large--line-height: 2.5rem;
--text-headline-large--letter-spacing: 0rem;
--text-headline-large--font-weight: 400;
--text-headline-small: 1.5rem;
--text-headline-small--line-height: 2rem;
--text-headline-small--letter-spacing: 0rem;
--text-headline-small--font-weight: 400;
--text-title-medium: 1rem;
--text-title-medium--line-height: 1.5rem;
--text-title-medium--letter-spacing: 0.0125rem;
--text-title-medium--font-weight: 500;
--text-label-large: 0.875rem;
--text-label-large--line-height: 1.25rem;
--text-label-large--letter-spacing: 0.00625rem;
--text-label-large--font-weight: 500;
}
}
@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 {
.state-layer {
position: relative;
&::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background-color: var(--state-layer-color, currentColor);
opacity: 0;
pointer-events: none;
transition: opacity var(--md-sys-motion-spring-fast-effects-duration) var(--md-sys-motion-spring-fast-effects);
}
@media (hover: hover) {
&:hover::before {
opacity: var(--md-sys-state-hover-opacity);
}
}
&:focus-visible::before, &[data-highlighted]::before {
opacity: var(--md-sys-state-focus-opacity);
}
&:active:not(:has(> [data-slot="ripple"]))::before, &[data-popup-open]::before {
opacity: var(--md-sys-state-pressed-opacity);
}
&:disabled::before, &[data-disabled]::before, &[aria-disabled="true"]::before {
opacity: 0;
}
}
.state-layer-circle {
position: relative;
&::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: var(--state-layer-size, 40px);
height: var(--state-layer-size, 40px);
translate: -50% -50%;
border-radius: 9999px;
background-color: var(--state-layer-color, currentColor);
opacity: 0;
pointer-events: none;
transition: opacity var(--md-sys-motion-spring-fast-effects-duration) var(--md-sys-motion-spring-fast-effects);
}
@media (hover: hover) {
&:hover::before {
opacity: var(--md-sys-state-hover-opacity);
}
}
&:focus-visible::before {
opacity: var(--md-sys-state-focus-opacity);
}
&:active::before {
opacity: var(--md-sys-state-pressed-opacity);
}
&[data-disabled]::before {
opacity: 0;
}
}
.relative {
position: relative;
}
.inline-flex {
display: inline-flex;
}
.size-8 {
width: calc(var(--spacing) * 8);
height: calc(var(--spacing) * 8);
}
.size-10 {
width: calc(var(--spacing) * 10);
height: calc(var(--spacing) * 10);
}
.size-14 {
width: calc(var(--spacing) * 14);
height: calc(var(--spacing) * 14);
}
.size-24 {
width: calc(var(--spacing) * 24);
height: calc(var(--spacing) * 24);
}
.size-34 {
width: calc(var(--spacing) * 34);
height: calc(var(--spacing) * 34);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
.h-10 {
height: calc(var(--spacing) * 10);
}
.h-14 {
height: calc(var(--spacing) * 14);
}
.h-24 {
height: calc(var(--spacing) * 24);
}
.h-34 {
height: calc(var(--spacing) * 34);
}
.w-\[28px\]\! {
width: 28px !important;
}
.w-\[32px\]\! {
width: 32px !important;
}
.w-\[40px\]\! {
width: 40px !important;
}
.w-\[48px\]\! {
width: 48px !important;
}
.w-\[52px\]\! {
width: 52px !important;
}
.w-\[64px\]\! {
width: 64px !important;
}
.w-\[72px\]\! {
width: 72px !important;
}
.w-\[104px\]\! {
width: 104px !important;
}
.w-\[128px\]\! {
width: 128px !important;
}
.w-\[184px\]\! {
width: 184px !important;
}
.shrink-0 {
flex-shrink: 0;
}
.grow {
flex-grow: 1;
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.cursor-pointer {
cursor: pointer;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.gap-3 {
gap: calc(var(--spacing) * 3);
}
.gap-4 {
gap: calc(var(--spacing) * 4);
}
.overflow-hidden {
overflow: hidden;
}
.rounded-\(--btn-r\) {
border-radius: var(--btn-r);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-outline-variant {
border-color: var(--md-sys-color-outline-variant);
}
.border-transparent {
border-color: transparent;
}
.bg-error {
background-color: var(--md-sys-color-error);
}
.bg-primary {
background-color: var(--md-sys-color-primary);
}
.bg-secondary-container {
background-color: var(--md-sys-color-secondary-container);
}
.bg-surface-container-low {
background-color: var(--md-sys-color-surface-container-low);
}
.bg-tertiary-container {
background-color: var(--md-sys-color-tertiary-container);
}
.bg-transparent {
background-color: transparent;
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.px-6 {
padding-inline: calc(var(--spacing) * 6);
}
.px-12 {
padding-inline: calc(var(--spacing) * 12);
}
.px-16 {
padding-inline: calc(var(--spacing) * 16);
}
.text-headline-large {
font-size: var(--text-headline-large);
line-height: var(--tw-leading, var(--text-headline-large--line-height));
letter-spacing: var(--tw-tracking, var(--text-headline-large--letter-spacing));
font-weight: var(--tw-font-weight, var(--text-headline-large--font-weight));
}
.text-headline-small {
font-size: var(--text-headline-small);
line-height: var(--tw-leading, var(--text-headline-small--line-height));
letter-spacing: var(--tw-tracking, var(--text-headline-small--letter-spacing));
font-weight: var(--tw-font-weight, var(--text-headline-small--font-weight));
}
.text-label-large {
font-size: var(--text-label-large);
line-height: var(--tw-leading, var(--text-label-large--line-height));
letter-spacing: var(--tw-tracking, var(--text-label-large--letter-spacing));
font-weight: var(--tw-font-weight, var(--text-label-large--font-weight));
}
.text-title-medium {
font-size: var(--text-title-medium);
line-height: var(--tw-leading, var(--text-title-medium--line-height));
letter-spacing: var(--tw-tracking, var(--text-title-medium--letter-spacing));
font-weight: var(--tw-font-weight, var(--text-title-medium--font-weight));
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-on-error {
color: var(--md-sys-color-on-error);
}
.text-on-primary {
color: var(--md-sys-color-on-primary);
}
.text-on-secondary-container {
color: var(--md-sys-color-on-secondary-container);
}
.text-on-surface-variant {
color: var(--md-sys-color-on-surface-variant);
}
.text-on-tertiary-container {
color: var(--md-sys-color-on-tertiary-container);
}
.text-primary {
color: var(--md-sys-color-primary);
}
.underline-offset-4 {
text-underline-offset: 4px;
}
.shadow {
--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);
}
.shadow-elevation-1 {
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.3)), 0 1px 3px 1px var(--tw-shadow-color, rgb(0 0 0 / 0.15));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.focus-ring {
outline: none;
&:focus-visible {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.transition-shape {
transition-property: border-radius, background-color, color, border-color, box-shadow, outline-color, padding, width, min-width, flex-grow;
transition-duration: var(--md-sys-motion-spring-default-effects-duration), var(--md-sys-motion-spring-fast-effects-duration), var(--md-sys-motion-spring-fast-effects-duration), var(--md-sys-motion-spring-fast-effects-duration), var(--md-sys-motion-spring-fast-effects-duration), var(--md-sys-motion-spring-fast-effects-duration), var(--md-sys-motion-spring-fast-spatial-duration), var(--md-sys-motion-spring-fast-spatial-duration), var(--md-sys-motion-spring-fast-spatial-duration), var(--md-sys-motion-spring-fast-spatial-duration);
transition-timing-function: var(--md-sys-motion-spring-default-effects), var(--md-sys-motion-spring-fast-effects), var(--md-sys-motion-spring-fast-effects), var(--md-sys-motion-spring-fast-effects), var(--md-sys-motion-spring-fast-effects), var(--md-sys-motion-spring-fast-effects), var(--md-sys-motion-spring-fast-spatial), var(--md-sys-motion-spring-fast-spatial), var(--md-sys-motion-spring-fast-spatial), var(--md-sys-motion-spring-fast-spatial);
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.\[--btn-h2\:16px\] {
--btn-h2: 16px;
}
.\[--btn-h2\:20px\] {
--btn-h2: 20px;
}
.\[--btn-h2\:28px\] {
--btn-h2: 28px;
}
.\[--btn-h2\:48px\] {
--btn-h2: 48px;
}
.\[--btn-h2\:68px\] {
--btn-h2: 68px;
}
.\[--btn-r-pressed\:var\(--radius-lg\)\] {
--btn-r-pressed: var(--radius-lg);
}
.\[--btn-r-pressed\:var\(--radius-md\)\] {
--btn-r-pressed: var(--radius-md);
}
.\[--btn-r-pressed\:var\(--radius-sm\)\] {
--btn-r-pressed: var(--radius-sm);
}
.\[--btn-r-square\:var\(--radius-2xl\)\] {
--btn-r-square: var(--radius-2xl);
}
.\[--btn-r-square\:var\(--radius-lg\)\] {
--btn-r-square: var(--radius-lg);
}
.\[--btn-r-square\:var\(--radius-md\)\] {
--btn-r-square: var(--radius-md);
}
.\[--btn-r\:16px\] {
--btn-r: 16px;
}
.\[--btn-r\:20px\] {
--btn-r: 20px;
}
.\[--btn-r\:28px\] {
--btn-r: 28px;
}
.\[--btn-r\:48px\] {
--btn-r: 48px;
}
.\[--btn-r\:68px\] {
--btn-r: 68px;
}
.\[--btn-r\:var\(--btn-r-square\)\] {
--btn-r: var(--btn-r-square);
}
@media (hover: hover) {
.hover\:underline:hover {
text-decoration-line: underline;
}
.hover\:shadow-elevation-1:hover {
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.3)), 0 1px 3px 1px var(--tw-shadow-color, rgb(0 0 0 / 0.15));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.hover\:shadow-elevation-2:hover {
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.3)), 0 2px 6px 2px var(--tw-shadow-color, rgb(0 0 0 / 0.15));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
.disabled\:pointer-events-none:disabled {
pointer-events: none;
}
.disabled\:cursor-default:disabled {
cursor: default;
}
.disabled\:border-on-surface\/12\!:disabled {
border-color: var(--md-sys-color-on-surface) !important;
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--md-sys-color-on-surface) 12%, transparent) !important;
}
}
.disabled\:border-transparent\!:disabled {
border-color: transparent !important;
}
.disabled\:bg-on-surface\/10\!:disabled {
background-color: var(--md-sys-color-on-surface) !important;
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--md-sys-color-on-surface) 10%, transparent) !important;
}
}
.disabled\:bg-transparent\!:disabled {
background-color: transparent !important;
}
.disabled\:text-on-surface\/38\!:disabled {
color: var(--md-sys-color-on-surface) !important;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent) !important;
}
}
.disabled\:shadow-none\!:disabled {
--tw-shadow: 0 0 #0000 !important;
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow) !important;
}
.has-data-\[icon\=inline-end\]\:pr-3:has([data-icon="inline-end"]) {
padding-right: calc(var(--spacing) * 3);
}
.has-data-\[icon\=inline-end\]\:pr-5:has([data-icon="inline-end"]) {
padding-right: calc(var(--spacing) * 5);
}
.has-data-\[icon\=inline-start\]\:pl-3:has([data-icon="inline-start"]) {
padding-left: calc(var(--spacing) * 3);
}
.has-data-\[icon\=inline-start\]\:pl-5:has([data-icon="inline-start"]) {
padding-left: calc(var(--spacing) * 5);
}
.aria-expanded\:bg-on-surface-variant\/8[aria-expanded="true"] {
background-color: var(--md-sys-color-on-surface-variant);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--md-sys-color-on-surface-variant) 8%, transparent);
}
}
.aria-invalid\:border-error[aria-invalid="true"] {
border-color: var(--md-sys-color-error);
}
.data-press\:rounded-\(--btn-r-pressed\)[data-press] {
border-radius: var(--btn-r-pressed);
}
.data-press\:shadow-elevation-1[data-press] {
--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.3)), 0 1px 3px 1px var(--tw-shadow-color, rgb(0 0 0 / 0.15));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.data-press\:shadow-none[data-press] {
--tw-shadow: 0 0 #0000;
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.data-\[variant\=outline\]\:border-2[data-variant="outline"] {
border-style: var(--tw-border-style);
border-width: 2px;
}
.data-\[variant\=outline\]\:border-3[data-variant="outline"] {
border-style: var(--tw-border-style);
border-width: 3px;
}
.data-\[variant\=outlined\]\:border-2[data-variant="outlined"] {
border-style: var(--tw-border-style);
border-width: 2px;
}
.data-\[variant\=outlined\]\:border-3[data-variant="outlined"] {
border-style: var(--tw-border-style);
border-width: 3px;
}
.\[\&_svg\]\:pointer-events-none svg {
pointer-events: none;
}
.\[\&_svg\]\:shrink-0 svg {
flex-shrink: 0;
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-5 svg:not([class*='size-']) {
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-6 svg:not([class*='size-']) {
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-8 svg:not([class*='size-']) {
width: calc(var(--spacing) * 8);
height: calc(var(--spacing) * 8);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-10 svg:not([class*='size-']) {
width: calc(var(--spacing) * 10);
height: calc(var(--spacing) * 10);
}
.\[\&\>\*\:not\(\[data-slot\=ripple\]\)\]\:relative > :not([data-slot=ripple]) {
position: relative;
}
.\[\&\>\*\:not\(\[data-slot\=ripple\]\)\]\:z-1 > :not([data-slot=ripple]) {
z-index: 1;
}
}
: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 {
--md-sys-shape-scale: 1;
--md-sys-shape-corner-none: 0px;
--md-sys-shape-corner-extra-small: calc(4px * var(--md-sys-shape-scale));
--md-sys-shape-corner-small: calc(8px * var(--md-sys-shape-scale));
--md-sys-shape-corner-medium: calc(12px * var(--md-sys-shape-scale));
--md-sys-shape-corner-large: calc(16px * var(--md-sys-shape-scale));
--md-sys-shape-corner-large-increased: calc(20px * var(--md-sys-shape-scale));
--md-sys-shape-corner-extra-large: calc(28px * var(--md-sys-shape-scale));
--md-sys-shape-corner-extra-large-increased: calc(
32px * var(--md-sys-shape-scale)
);
--md-sys-shape-corner-extra-extra-large: calc(
48px * var(--md-sys-shape-scale)
);
--md-sys-shape-corner-full: 9999px;
--md-sys-state-hover-opacity: 0.08;
--md-sys-state-focus-opacity: 0.1;
--md-sys-state-pressed-opacity: 0.1;
--md-sys-state-dragged-opacity: 0.16;
--md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1);
--md-sys-motion-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
--md-sys-motion-easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15);
--md-sys-motion-easing-standard: cubic-bezier(0.2, 0, 0, 1);
--md-sys-motion-easing-standard-decelerate: cubic-bezier(0, 0, 0, 1);
--md-sys-motion-easing-standard-accelerate: cubic-bezier(0.3, 0, 1, 1);
}
:root, .dark {
--background: var(--md-sys-color-surface);
--foreground: var(--md-sys-color-on-surface);
--card: var(--md-sys-color-surface-container-low);
--card-foreground: var(--md-sys-color-on-surface);
--popover: var(--md-sys-color-surface-container);
--popover-foreground: var(--md-sys-color-on-surface);
--primary: var(--md-sys-color-primary);
--primary-foreground: var(--md-sys-color-on-primary);
--secondary: var(--md-sys-color-secondary);
--secondary-foreground: var(--md-sys-color-on-secondary);
--muted: var(--md-sys-color-surface-container-highest);
--muted-foreground: var(--md-sys-color-on-surface-variant);
--accent: var(--md-sys-color-secondary-container);
--accent-foreground: var(--md-sys-color-on-secondary-container);
--destructive: var(--md-sys-color-error);
--destructive-foreground: var(--md-sys-color-on-error);
--border: var(--md-sys-color-outline-variant);
--input: var(--md-sys-color-outline);
--ring: var(--md-sys-color-secondary);
--chart-1: var(--md-sys-color-primary);
--chart-2: var(--md-sys-color-tertiary);
--chart-3: var(--md-sys-color-secondary);
--chart-4: var(--md-sys-color-primary-fixed-dim);
--chart-5: var(--md-sys-color-tertiary-fixed-dim);
--radius: var(--md-sys-shape-corner-large);
--sidebar: var(--md-sys-color-surface-container-low);
--sidebar-foreground: var(--md-sys-color-on-surface-variant);
--sidebar-primary: var(--md-sys-color-primary);
--sidebar-primary-foreground: var(--md-sys-color-on-primary);
--sidebar-accent: var(--md-sys-color-secondary-container);
--sidebar-accent-foreground: var(--md-sys-color-on-secondary-container);
--sidebar-border: var(--md-sys-color-outline-variant);
--sidebar-ring: var(--md-sys-color-secondary);
}
@keyframes m3-progress-bar-1 {
0% {
left: -145.167%;
x: -145.167%;
width: 8%;
}
20% {
left: -113.333%;
x: -113.333%;
width: 48%;
}
60% {
left: 56.333%;
x: 56.333%;
width: 78%;
}
100% {
left: 100%;
x: 100%;
width: 8%;
}
}
@keyframes m3-progress-bar-2 {
0% {
left: -54.888%;
x: -54.888%;
width: 8%;
}
20% {
left: -20%;
x: -20%;
width: 48%;
}
60% {
left: 60%;
x: 60%;
width: 78%;
}
100% {
left: 160%;
x: 160%;
width: 8%;
}
}
@keyframes m3-wave-slide {
to {
transform: translateX(-40px);
}
}
@keyframes m3-spinner-rotate {
to {
transform: rotate(360deg);
}
}
@keyframes m3-spinner-arc {
0% {
stroke-dashoffset: 97;
transform: rotate(0deg);
}
50% {
stroke-dashoffset: 25;
transform: rotate(45deg);
}
100% {
stroke-dashoffset: 97;
transform: rotate(360deg);
}
}
@keyframes m3-cp-expand-arc {
0% {
transform: rotate(265deg);
}
50% {
transform: rotate(130deg);
}
100% {
transform: rotate(265deg);
}
}
@keyframes m3-cp-rotate-arc {
12.5% {
transform: rotate(135deg);
}
25% {
transform: rotate(270deg);
}
37.5% {
transform: rotate(405deg);
}
50% {
transform: rotate(540deg);
}
62.5% {
transform: rotate(675deg);
}
75% {
transform: rotate(810deg);
}
87.5% {
transform: rotate(945deg);
}
100% {
transform: rotate(1080deg);
}
}
@keyframes m3-cp-linear-rotate {
to {
transform: rotate(360deg);
}
}
@keyframes m3-cp-spin-reverse {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}
@keyframes m3-cp-wavy-spin {
0% {
transform: rotate(0deg);
}
10% {
transform: rotate(90deg);
}
100% {
transform: rotate(360deg);
}
}
:root {
--md-sys-color-background: #fef7ff;
--md-sys-color-error: #b3261e;
--md-sys-color-error-container: #f9dedc;
--md-sys-color-error-dim: #b3261e;
--md-sys-color-inverse-on-surface: #f5eff7;
--md-sys-color-inverse-primary: #d0bcff;
--md-sys-color-inverse-surface: #322f35;
--md-sys-color-on-background: #1d1b20;
--md-sys-color-on-error: #ffffff;
--md-sys-color-on-error-container: #8c1d18;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-on-primary-container: #4f378b;
--md-sys-color-on-primary-fixed: #21005d;
--md-sys-color-on-primary-fixed-variant: #4f378b;
--md-sys-color-on-secondary: #ffffff;
--md-sys-color-on-secondary-container: #4a4458;
--md-sys-color-on-secondary-fixed: #1d192b;
--md-sys-color-on-secondary-fixed-variant: #4a4458;
--md-sys-color-on-surface: #1d1b20;
--md-sys-color-on-surface-variant: #49454f;
--md-sys-color-on-tertiary: #ffffff;
--md-sys-color-on-tertiary-container: #633b48;
--md-sys-color-on-tertiary-fixed: #31111d;
--md-sys-color-on-tertiary-fixed-variant: #633b48;
--md-sys-color-outline: #79747e;
--md-sys-color-outline-variant: #cac4d0;
--md-sys-color-primary: #6750a4;
--md-sys-color-primary-container: #eaddff;
--md-sys-color-primary-dim: #6750a4;
--md-sys-color-primary-fixed: #eaddff;
--md-sys-color-primary-fixed-dim: #d0bcff;
--md-sys-color-scrim: #000000;
--md-sys-color-secondary: #625b71;
--md-sys-color-secondary-container: #e8def8;
--md-sys-color-secondary-dim: #625b71;
--md-sys-color-secondary-fixed: #e8def8;
--md-sys-color-secondary-fixed-dim: #ccc2dc;
--md-sys-color-shadow: #000000;
--md-sys-color-surface: #fef7ff;
--md-sys-color-surface-bright: #fef7ff;
--md-sys-color-surface-container: #f3edf7;
--md-sys-color-surface-container-high: #ece6f0;
--md-sys-color-surface-container-highest: #e6e0e9;
--md-sys-color-surface-container-low: #f7f2fa;
--md-sys-color-surface-container-lowest: #ffffff;
--md-sys-color-surface-dim: #ded8e1;
--md-sys-color-surface-tint: #6750a4;
--md-sys-color-surface-variant: #e7e0ec;
--md-sys-color-tertiary: #7d5260;
--md-sys-color-tertiary-container: #ffd8e4;
--md-sys-color-tertiary-dim: #7d5260;
--md-sys-color-tertiary-fixed: #ffd8e4;
--md-sys-color-tertiary-fixed-dim: #efb8c8;
}
.dark {
--md-sys-color-background: #141218;
--md-sys-color-error: #f2b8b5;
--md-sys-color-error-container: #8c1d18;
--md-sys-color-error-dim: #f2b8b5;
--md-sys-color-inverse-on-surface: #322f35;
--md-sys-color-inverse-primary: #6750a4;
--md-sys-color-inverse-surface: #e6e0e9;
--md-sys-color-on-background: #e6e0e9;
--md-sys-color-on-error: #601410;
--md-sys-color-on-error-container: #f9dedc;
--md-sys-color-on-primary: #381e72;
--md-sys-color-on-primary-container: #eaddff;
--md-sys-color-on-primary-fixed: #21005d;
--md-sys-color-on-primary-fixed-variant: #4f378b;
--md-sys-color-on-secondary: #332d41;
--md-sys-color-on-secondary-container: #e8def8;
--md-sys-color-on-secondary-fixed: #1d192b;
--md-sys-color-on-secondary-fixed-variant: #4a4458;
--md-sys-color-on-surface: #e6e0e9;
--md-sys-color-on-surface-variant: #cac4d0;
--md-sys-color-on-tertiary: #492532;
--md-sys-color-on-tertiary-container: #ffd8e4;
--md-sys-color-on-tertiary-fixed: #31111d;
--md-sys-color-on-tertiary-fixed-variant: #633b48;
--md-sys-color-outline: #938f99;
--md-sys-color-outline-variant: #49454f;
--md-sys-color-primary: #d0bcff;
--md-sys-color-primary-container: #4f378b;
--md-sys-color-primary-dim: #d0bcff;
--md-sys-color-primary-fixed: #eaddff;
--md-sys-color-primary-fixed-dim: #d0bcff;
--md-sys-color-scrim: #000000;
--md-sys-color-secondary: #ccc2dc;
--md-sys-color-secondary-container: #4a4458;
--md-sys-color-secondary-dim: #ccc2dc;
--md-sys-color-secondary-fixed: #e8def8;
--md-sys-color-secondary-fixed-dim: #ccc2dc;
--md-sys-color-shadow: #000000;
--md-sys-color-surface: #141218;
--md-sys-color-surface-bright: #3b383e;
--md-sys-color-surface-container: #211f26;
--md-sys-color-surface-container-high: #2b2930;
--md-sys-color-surface-container-highest: #36343b;
--md-sys-color-surface-container-low: #1d1b20;
--md-sys-color-surface-container-lowest: #0f0d13;
--md-sys-color-surface-dim: #141218;
--md-sys-color-surface-tint: #d0bcff;
--md-sys-color-surface-variant: #49454f;
--md-sys-color-tertiary: #efb8c8;
--md-sys-color-tertiary-container: #633b48;
--md-sys-color-tertiary-dim: #efb8c8;
--md-sys-color-tertiary-fixed: #ffd8e4;
--md-sys-color-tertiary-fixed-dim: #efb8c8;
}
:root, [data-motion="expressive"] {
--md-sys-motion-spring-fast-spatial: linear(
0,
0.0206,
0.0754,
0.1544,
0.2492,
0.3524,
0.458,
0.5613,
0.6588,
0.7479,
0.8269,
0.895,
0.952,
0.9981,
1.0338,
1.0603,
1.0783,
1.0893,
1.0942,
1.0943,
1.0906,
1.0842,
1.0759,
1.0665,
1.0565,
1.0465,
1.037,
1.0281,
1.0201,
1.0131,
1.0072,
1.0023,
0.9984,
0.9955,
0.9934,
0.992,
0.9912,
0.991,
0.9912,
0.9917,
0.9924,
0.9932,
0.9942,
0.9951,
0.996,
0.9969,
0.9977,
0.9984,
1
);
--md-sys-motion-spring-fast-spatial-duration: 360ms;
--md-sys-motion-spring-default-spatial: linear(
0,
0.0145,
0.0527,
0.1076,
0.1736,
0.2461,
0.3214,
0.3968,
0.4702,
0.54,
0.6051,
0.665,
0.7193,
0.7679,
0.8108,
0.8483,
0.8807,
0.9083,
0.9316,
0.951,
0.9669,
0.9798,
0.99,
0.998,
1.004,
1.0085,
1.0116,
1.0136,
1.0147,
1.0151,
1.0151,
1.0146,
1.0138,
1.0128,
1.0118,
1.0106,
1.0095,
1.0084,
1.0073,
1.0063,
1.0053,
1.0045,
1.0037,
1.0031,
1.0025,
1.002,
1.0015,
1.0011,
1
);
--md-sys-motion-spring-default-spatial-duration: 440ms;
--md-sys-motion-spring-slow-spatial: linear(
0,
0.0142,
0.0517,
0.1057,
0.1706,
0.2421,
0.3166,
0.3912,
0.464,
0.5334,
0.5984,
0.6583,
0.7127,
0.7615,
0.8047,
0.8426,
0.8755,
0.9036,
0.9274,
0.9473,
0.9638,
0.9771,
0.9878,
0.9962,
1.0027,
1.0074,
1.0108,
1.0131,
1.0144,
1.0151,
1.0151,
1.0148,
1.0141,
1.0132,
1.0122,
1.0111,
1.0099,
1.0088,
1.0077,
1.0067,
1.0057,
1.0049,
1.0041,
1.0034,
1.0027,
1.0022,
1.0017,
1.0013,
1
);
--md-sys-motion-spring-slow-spatial-duration: 600ms;
--md-sys-motion-spring-fast-effects: linear(
0,
0.0576,
0.1807,
0.3214,
0.4558,
0.5737,
0.6718,
0.7508,
0.8128,
0.8606,
0.897,
0.9244,
0.9448,
0.9599,
0.9709,
0.979,
0.9849,
0.9892,
0.9923,
0.9945,
0.9961,
0.9972,
0.998,
0.9986,
1
);
--md-sys-motion-spring-fast-effects-duration: 150ms;
--md-sys-motion-spring-default-effects: linear(
0,
0.0616,
0.1912,
0.3374,
0.4751,
0.594,
0.6916,
0.7689,
0.8288,
0.8743,
0.9084,
0.9337,
0.9523,
0.9658,
0.9756,
0.9826,
0.9877,
0.9913,
0.9939,
0.9957,
0.997,
0.9979,
0.9985,
0.999,
1
);
--md-sys-motion-spring-default-effects-duration: 240ms;
--md-sys-motion-spring-slow-effects: linear(
0,
0.0586,
0.1833,
0.3253,
0.4606,
0.5788,
0.6768,
0.7554,
0.8169,
0.8641,
0.8999,
0.9268,
0.9467,
0.9614,
0.9722,
0.98,
0.9857,
0.9898,
0.9927,
0.9948,
0.9963,
0.9974,
0.9982,
0.9987,
1
);
--md-sys-motion-spring-slow-effects-duration: 330ms;
}
@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-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
syntax: "*";
inherits: false;
}
@property --tw-ring-offset-width {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}
@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@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-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-shadow-alpha: 100%;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-inset-shadow-alpha: 100%;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-outline-style: solid;
}
}
}