diff --git a/app/ui/lib/Button.tsx b/app/ui/lib/Button.tsx index a0ec80ef7..d7b3262d0 100644 --- a/app/ui/lib/Button.tsx +++ b/app/ui/lib/Button.tsx @@ -6,7 +6,6 @@ * Copyright Oxide Computer Company */ import cn from 'classnames' -import * as m from 'motion/react-m' import { type MouseEventHandler, type ReactNode } from 'react' import { type BadgeColor } from '@oxide/design-system/ui' @@ -120,25 +119,19 @@ export const Button = ({ {...rest} > {loading && ( - + - + )} - {children} - + ) diff --git a/app/ui/styles/components/button.css b/app/ui/styles/components/button.css index 4e0ba40b6..e9570f8b4 100644 --- a/app/ui/styles/components/button.css +++ b/app/ui/styles/components/button.css @@ -8,6 +8,11 @@ .ox-button { @apply relative; + /* + * Shared by the spinner keyframes and the .button-content transition below. + * Approximates Motion's { type: 'spring', duration: 0.3, bounce: 0 }. + */ + --button-loading-easing: cubic-bezier(0.35, 0.98, 0.35, 1); &:after { content: ''; @@ -72,3 +77,31 @@ .active-clicked:active:not(.visually-disabled) { @apply motion-safe:translate-y-px; } + +/* Animating transform is deliberate: the spinner's centering utilities set the + * separate translate property, which composes with transform instead of being + * overridden by it. */ +@keyframes button-spinner-in { + from { + opacity: 0; + transform: translateY(-25px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: no-preference) { + .ox-button .button-spinner-in { + animation: button-spinner-in 0.3s var(--button-loading-easing); + } + + /* Button label; swaps out as the spinner drops in. Loading state is toggled + * with utility classes in Button.tsx. */ + .ox-button .button-content { + transition: + opacity 0.3s var(--button-loading-easing), + translate 0.3s var(--button-loading-easing); + } +}