TL;DR Most pricing card motion needs no library. Use @property to animate a conic gradient border on the featured plan, transform and shadow transitions for hover, a CSS keyframe with a per-card delay for the reveal (started by one IntersectionObserver or by animation-timeline: view() where supported), and stroke-dashoffset for the checkmarks. Reach for Motion when cards change layout or need exit animations.
When Cursor or v0 builds a pricing section and you ask for "some animation", it often adds Framer Motion (now called Motion) and wraps every card in motion.div with whileHover and whileInView. That works, but it ships the library to a page that is mostly static, and every card becomes a client component. I make animated React components at qodesign, and for pricing cards I reach for plain CSS first.
Here are the four motions most pricing sections want, each in Tailwind v4 and a few lines of CSS.
That is the Pricing Switch, a free piece on qodesign with an odometer roll on the numbers. The cards below are a lighter base you can build on.
@property registers --angle as an angle, so the browser can interpolate it. Without the registration, a custom property jumps from start to end with no frames in between.
A thin bright arc travels around the featured card and the rest of the border stays grey. Keep the arc short; a full rainbow border pulls attention from the price. @property works in current Chrome, Edge, Safari and Firefox, and older browsers just show a still gradient.
Three choices matter here. The translate property moves the card without pushing its neighbours, so the grid stays still. It is separate from transform, which the reveal animation in the next step holds with both; if the hover used transform too, the finished animation would win and the lift would never show. And @media (hover: hover) keeps the lift off touch screens, where a tap would leave the card stuck in the raised state.
One small client component adds the class. It watches the grid once, so the rest of the section can stay a server component:
tsx
"use client";import { useEffect, useRef } from "react";export function RevealOnView({ children }: { children: React.ReactNode }) { const ref = useRef<HTMLDivElement>(null); useEffect(() => { const el = ref.current; if (!el) return; const io = new IntersectionObserver( ([entry]) => { if (!entry.isIntersecting) return; el.querySelectorAll(".reveal").forEach((c) => c.classList.add("is-visible")); io.disconnect(); }, { threshold: 0.2 }, ); io.observe(el); return () => io.disconnect(); }, []); return <div ref={ref}>{children}</div>;}
Wrap the grid: <RevealOnView><PricingCards plans={plans} /></RevealOnView>.
There is also a CSS-only route. animation-timeline: view() ties an animation to the element's position in the viewport, with no JavaScript. Browser support is still uneven, so put it behind @supports (animation-timeline: view()) and keep the observer as the fallback.
The content is fully visible without any motion. That also covers a failure case: if JavaScript fails to load, cards hidden by .reveal would stay invisible. If that worries you, add the reveal class from the client component instead of the markup.
Yes. Hover lift, staggered reveal, gradient borders and drawn checkmarks all work with CSS transitions and keyframes in Tailwind. A small IntersectionObserver starts the reveal, and the rest of the section can stay a server component.
Register a custom property with @property as an angle, use it in a conic-gradient on a wrapper with 1px padding, and animate the property from 0deg to 360deg with a keyframe. The inner element keeps the solid card background.
Set a custom property with each card's index, such as --i, and use calc(var(--i) * 80ms) as the animation-delay. Every card shares one keyframe and starts a little later than the one before.
Use it when the layout itself changes, for example when cards reorder or resize between monthly and yearly, or when cards need exit animations. For entrance, hover and looping effects, CSS is lighter and enough.