
Pure CSS Polygon Morph Play Pause
Published
25 hand-coded CSS play/pause buttons for audio players, HTML5 video controls, podcast UIs, meditation apps, and media dashboards — pure CSS polygon morph (no JavaScript), custom HTML5 video overlay, glassmorphism audio toggle, neumorphic soft UI, Spotify-inspired circular player, cyberpunk neon, WAI-ARIA accessible pattern (WCAG 2.2 AA reference), vinyl / equalizer / waveform / cassette / voice-note skeuomorphic controls, iOS sliding pill, Material Design FAB + ripple, magnetic hover, conic-gradient rotating rim, podcast progress ring, and live-broadcast badge. 16 Pure CSS + 9 Light JS. Scoped under .pp-NN for no-collision pasting, prefers-reduced-motion guarded per WCAG 2.3.3, framework-agnostic (React, Vue, Svelte, Astro, Next, Nuxt).
Related51 CSS Buttons34 CSS Floating Buttons47 CSS Toggles & Switches

Published

<video> with the native controls off, a centered glass play button over a gradient scrim, and a real scrub bar wired to timeupdate. The overlay fades out while playing and returns on pause or hover — the behaviour users expect from YouTube-class players — and the whole control surface is keyboard operable, with Space and K bound to toggle like a native player.Published

Published

Published

Published

Published

<button> carrying aria-pressed, a 44 × 44 px target, a 3:1 focus indicator that survives forced-colors mode, a polite live region for state changes, and a side panel that prints exactly what a screen reader announces at each step — so you can see the semantics, not just trust them. This is the demo to copy when the component ships to production.Published

d property, which lets you transition an SVG path's geometry the same way you transition a color. No GreenSock, no Flubber, no JS. Browsers without d interpolation get a tasteful rotate-and-crossfade instead, gated behind an @supports query, so the component degrades instead of breaking.PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

@property angle feeding a conic-gradient, clipped to a 3px rim by the mask-composite border trick. It is the modern way to build gradient borders (no wrapper divs, no background-clip hacks), and because the angle is a registered custom property, the browser can interpolate it on the compositor.PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated

PublishedUpdated
Tweak the exact look in our visual generators — no signup, instant copy-paste.
@media (prefers-reduced-motion: reduce) { .pp-NN * { animation: none !important; }} per WCAG 2.3.3. Non-negotiable in EU EAA (took effect June 28, 2025 with €5M per country max fines) and Section 508 federal contracts.d attribute of a <path> between a triangle path ('M 8 6 L 8 26 L 24 16 Z') and pause-bars path ('M 8 6 L 12 6 L 12 26 L 8 26 Z M 20 6 L 24 6 L 24 26 L 20 26 Z'). THE TRAP: browsers do NOT natively animate the SVG d attribute in pure CSS — Chrome, Safari, Firefox all ignore transition: d 0.3s. You need either (a) SMIL <animate> (deprecated, avoid), (b) JavaScript path interpolation (Flubber.js or manual), or (c) the CSS Motion Path Level 2 spec's d: path() declaration (experimental, Chrome-only). Demo 01 in this collection uses pure CSS clip-path instead (see technique 2). (2) Pure CSS clip-path polygon morph (Demo 01 Pure CSS Polygon Morph Play Pause) — the technique this collection recommends. Two clip-path polygons are defined (triangle points, pause-bars points) and CSS transitions the clip-path property between them. The browser interpolates polygon point coordinates natively — no JavaScript, no library. Zero DOM changes, zero repaints outside the button. Works in Chrome 55+ / Safari 13.1+ / Firefox 81+ — Baseline since 2020. Full-quality morph animation in ~15 lines of CSS. (3) 2-icon swap (used by Demo 08 CSS Play Button Icon Morph Animation and most tier-1 sites) — two SVG icons stacked, one fades/scales in while the other fades/scales out. Simpler code, works in every browser back to IE10, but less premium-feeling than a true morph. Reach for this when you need bulletproof compatibility or when the transition is fast (< 150ms) — the eye can't distinguish a morph from a swap at short durations. Which to use when: clip-path polygon for premium marketing sites where the button IS the hero (music player hero, video landing page, meditation app tap-target). 2-icon swap for functional UI (in-line audio players, video controls, background media). SVG path morph only if you're already invested in a path-morphing library. Zero of these techniques requires more than 30 lines of CSS — none of them requires a library. Compare to shipping Lottie (~120KB gzipped for the runtime + the exported .json file) or Framer Motion (~40KB gzipped) or anime.js (~15KB) — the pure-CSS approaches this collection ships are 0KB, INP-safe (compositor-only animation), and CLS 0.controls attribute on your <video> or <audio> element. The browser's default UI is ugly, unbranded, and inconsistent across Chrome / Safari / Firefox — that's why every tier-1 video tool ships custom controls. (2) Real <video> or <audio> element — never a <div> background-video hack. Real media elements support keyboard shortcuts natively (Space for play/pause, arrows for scrub, F for fullscreen), preload/lazy-load hints, subtitle tracks, and screen-reader announcements. (3) A play/pause button overlaying the video (Demo 02 pattern) or beside the audio waveform (Demo 20 Podcast pattern). Wire it with btn.addEventListener('click', () => video.paused ? video.play() : video.pause()). (4) The autoplay policy trap: since 2018, Chrome / Safari / Firefox block audio autoplay without user gesture — video.play() returns a Promise that rejects with 'NotAllowedError' if called before user interaction. Wrap in try/catch: video.play().catch(e => { /* show fallback UI */ }). If you need audio to autoplay (rare), start muted (<video muted autoplay>) and unmute on user click. (5) Sync UI state to media events: listen for video.addEventListener('play', ...) and video.addEventListener('pause', ...) to update your button's aria-pressed and icon state. Do NOT rely on your click handler alone — the browser can pause the video (user hits Space, ended event, external control) without your click handler firing. (6) Backdrop-blur click-to-play overlay: place a full-viewport backdrop-filter: blur(12px) overlay above the video with a centered play button at rest. On first click, remove the overlay and start playback. The pattern Loom / Wistia / Vimeo / Descript / Riverside all use — increases perceived quality and gives you a click-to-load facade opportunity (defer the video element until user gesture, save ~500KB of HLS runtime). Tier-1 CPM: video-hosting SaaS $15-30 CPM (Loom / Wistia / Vimeo / Descript / Riverside / Zoom / Google Meet recording), EdTech $20-35 (Coursera / Udemy / Skillshare / MasterClass — MasterClass runs $40+ CPM on their premium course preview pages), enterprise video (Panopto / Kaltura / Brightcove) $25-45.oklch(0.72 0.19 148) / #1DB954) so it reads as 'the Spotify pattern' to design-conscious visitors. Demo 02 (Custom HTML5 Video Player Overlay) is the Loom / Wistia / Vimeo / Descript / Riverside / Zoom / Google Meet recording SaaS video-tool pattern — video-hosting CPM $15-30, EdTech $20-35 (Coursera / Udemy / Skillshare / MasterClass), enterprise video $25-45 (Panopto / Kaltura / Brightcove). Demo 07 (Accessible WAI-ARIA Play/Pause Button — WCAG 2.2) is the enterprise / gov / healthcare procurement-compliance pattern — enterprise SaaS $25-45 CPM (Salesforce / ServiceNow / Workday audio+video enterprise verticals with accessibility as literal RFP checklist items), government $30-60 CPM (Section 508 mandatory since 1998, WCAG 2.2 AA the enforcement floor), healthcare $30-50 CPM (Epic MyChart / Zocdoc / Teladoc telehealth video + Section 508 + HIPAA compliance premium). Demo 20 (Podcast Play with SVG Progress Ring) is the podcast platform pattern — Apple Podcasts / Spotify Podcasts / Overcast / Pocket Casts / Castro / Snipd / Amazon Music Podcasts — podcast CPM $10-20, premium-content niches (finance podcasts $25-45 / true crime $15-25 / business B2B $20-35 like Lex Fridman / Tim Ferriss / All-In / My First Million). Demo 04 (Neumorphic Soft UI Media Control) is the meditation / wellness app pattern — Calm / Headspace / Insight Timer / Waking Up / Balance / Aura — wellness CPM $15-25, subscription-conversion pages $20-35 (Calm and Headspace both run ~$70/year subscriptions with generous free-trial CPCs). Demo 25 (Live Stream Broadcast Badge Control) is the Twitch / YouTube Live / Facebook Live / Discord voice / Kick pattern — live-streaming CPM $8-30 (gaming Twitch $8-15, esports premium $20-40, live commerce $15-30 like TikTok Shop live sessions, live news streams $10-20). Every one of these very-high-CPM verticals rewards the WCAG 2.2 AA + zero-dependency freshness this collection ships because enterprise procurement AND ad-quality-score algorithms both reward accessible, INP-safe UI.transform and opacity get keyframed. NEVER top, left, margin, padding, width, height — every frame triggers layout+paint on the main thread, blocking interaction handling. clip-path and filter are safe (compositor-friendly on Chromium since 108). backdrop-filter is on the compositor thread but has a GPU cost — cap the blur radius at 20px. (2) will-change discipline: use will-change: transform BEFORE the animation starts (via :hover or JS class add), remove it AFTER — don't leave will-change permanently declared, it consumes GPU memory. (3) rAF-throttled JS handlers: the 9 Light-JS demos in this collection each ship an idempotent IIFE under 30 lines. Event handlers use requestAnimationFrame for state-driven UI updates (Demo 20's progress ring tick, Demo 25's live badge pulse, Demo 21's magnetic hover pointer track). No setInterval at 10ms polling loops — Demo 20's SVG progress ring runs at 90ms interval which is 11 fps (imperceptible visual smoothness cost, zero INP tax). (4) CLS 0 guarantee: every button has explicit width AND height (or aspect-ratio) declared before any animation. No layout shift when the icon morphs, no shift when the button enters/exits playing state, no shift when the ripple wave spawns (Demo 23). The ripple uses position: absolute so it never affects sibling layout. (5) prefers-reduced-motion tiered fallback: honors WCAG 2.3.3 AND acts as an INP protection lever for older devices (users with reduced-motion enabled often correlate with older/lower-end hardware). Reduced-motion drops all continuous animations to animation: none, cutting JS + paint load. The tier-1 lift: Google Ads Quality Score is derived from Core Web Vitals since 2023 — a Good INP + CLS 0 combo lowers your CPC on ad-supported pages by ~20-40% and lifts organic-search ranking on competitive keyword clusters. For a media-tool site running $1-5 CPCs, that's real revenue. Every one of these demos hits Good on INP + Good on CLS out of the box.class to className, drop the JSX into any server or client component. The 16 Pure CSS demos need NO 'use client' directive because they toggle state via checkbox-hack or CSS-only :has() — they're static SSR-safe HTML that works with JavaScript disabled. Only the 9 Light-JS demos (01 Polygon Morph aria-pressed toggle, 02 HTML5 Video Overlay, 05 Spotify audio hook, 07 WAI-ARIA reference, 08 Icon Morph, 20 Podcast Progress Ring, 21 Magnetic Hover, 23 Material Ripple, 25 Live Badge pulse) need client-side hydration, each under 30 lines of vanilla JavaScript. Vue 3 / Nuxt 3: accepts as-is, class is native. Svelte / SvelteKit: accepts as-is; wrap in a .svelte component for scoped styles. Astro: drop into any .astro component (this site is Astro; every demo renders SSR here). Remix / Solid.js / Qwik / Lit: accepts as-is. Compared to framework-specific media-control libraries: shadcn/ui has no dedicated PlayPause primitive — you compose from Toggle (~4KB Radix + React dep) + Icon. @radix-ui/react-toggle ships ~4KB gzipped + requires React 18+; provides aria-pressed handling but no visual style — you're still writing all 25 designs yourself on top of Radix. @mui/material IconButton is part of Material UI's ~85KB base bundle + Emotion runtime — locks you into MUI theming. @chakra-ui/button requires Chakra's ~50KB Emotion runtime + full theme provider. @headlessui/react Switch handles aria-checked but is switch pattern (wrong semantics for media — see FAQ 1). react-player is ~50KB gzipped for the wrapper + it lazy-loads the underlying provider SDK (YouTube ~90KB, Vimeo ~40KB, Wistia ~50KB, Loom via iframe) so you're at 100-200KB before your first play. Plyr (~65KB gzipped) and Video.js (~150KB gzipped) are the popular custom-controls libraries — both come with opinionated styling you'll fight to override to match your design system. The 25 buttons in this collection total 2-6KB gzipped per demo when you cherry-pick — an entire play/pause system for less than a single Radix primitive. The .pp-NN numeric-prefix scoping means all 25 patterns coexist without a single class collision, and the same prefix guarantees you can drop any demo into a codebase that already uses .play, .pause, .toggle, .btn, or .icon classes without renaming. MIT licensed, no attribution required, no signup, no build step.background: #1DB954 (hex) or the modern equivalent background: oklch(0.72 0.19 148) (perceptually equivalent, works with color-mix() gradient hover states). Spotify uses a subtle brand-tuned green that's slightly cooler than pure emerald — do NOT use #10B981 (Tailwind emerald-500) or the pattern reads as 'Spotify-adjacent' rather than 'Spotify'. (3) Black play triangle icon, offset 2-3px right to visually center inside the circle (perceptual centering — a triangle's visual weight sits left of its geometric center, so shift it right to LOOK centered). Icon size ~40% of button diameter (22-24px on a 56px button). (4) Hover: scale-up + brightness. :hover { transform: scale(1.05); background: oklch(0.76 0.19 148) } — 5% scale bump + slight lightness increase. Duration 150-200ms with cubic-bezier(0.4, 0, 0.2, 1) (Material easing). NEVER add a translate-Y lift — Spotify's play button stays in-plane; a hover lift breaks the 'anchored to content' feel. (5) Active: scale-down. :active { transform: scale(0.95) } — subtle press feedback, snaps back on release. Zero shadow / elevation on the button itself — Spotify's play button is flat, not FAB. (6) Focus-visible outline: :focus-visible { outline: 3px solid oklch(0.72 0.19 148 / 0.4); outline-offset: 3px } — a soft green halo, 3:1 contrast, tuned for keyboard users without disrupting the flat aesthetic. The playing state switches the icon to pause bars via CSS (the pattern in this demo uses opacity swap between two SVG icons; Demo 01 uses clip-path morph for a smoother transition). aria-pressed and aria-label update accordingly for screen readers. Tier-1 CPM lift: shipping the Spotify pattern on a music-streaming SaaS landing page (~$15-25 CPM) or a subscription-conversion page (~$25-35) means users instantly read the button as 'the professional pattern' — Baymard research shows brand-familiar UI patterns lift conversion 3-6% by lowering cognitive load. Zero legal issue with copying the visual pattern (colors, shape, size are functional design elements, not trademarked). Do NOT copy the Spotify wordmark or 3-bar Spotify logo — those ARE trademarks.stroke-dashoffset). (2) The stroke-dashoffset trick: set stroke-dasharray to the ring's circumference (2πr — for r=40, that's ~251). Then animate stroke-dashoffset from 251 (empty ring) to 0 (full ring). At any point, stroke-dashoffset = circumference × (1 - percentPlayed). This is the pattern EVERY podcast app uses because it's compositor-safe (SVG stroke properties don't trigger layout+paint on the main thread — INP-safe). (3) Wire to real timeupdate events: audio.addEventListener('timeupdate', () => { const pct = audio.currentTime / audio.duration; ring.style.strokeDashoffset = 251 * (1 - pct); }). Throttle to requestAnimationFrame if you're updating on every timeupdate (Chrome fires timeupdate ~4x per second, low enough to not need throttling for most cases). (4) Play button centered inside the ring. Standard circular play button (see Demo 05 Spotify recipe) sized to fit inside the ring with 8-12px padding. The play/pause icon morphs on state change; the ring keeps ticking regardless. (5) Rotate the ring -90 degrees: SVG circles start their stroke at 3 o'clock. Add transform: rotate(-90deg); transform-origin: center; to the ring so the fill starts from 12 o'clock (the intuitive top position, matching every analog clock and every podcast app). The visual language of tier-1 podcast apps: Apple Podcasts uses a purple ring (oklch(0.65 0.2 290)), Spotify Podcasts uses green (matches Spotify brand), Overcast uses a warmer orange, Pocket Casts uses red — each brand-tuned but the ring geometry + stroke-dashoffset animation is identical across all of them. Bonus: add a small time-remaining label below the button ('12:34 remaining') using a simple template literal that formats audio.duration - audio.currentTime — the pattern every podcast app ships. Tier-1 CPM: podcast CPM $10-20, premium podcast niches $25-45 (finance / business B2B / true crime / health), podcast subscription-conversion pages $15-30. Every tier-1 platform (Apple / Spotify / Overcast / Pocket Casts / Castro / Snipd / Amazon Music Podcasts) uses this exact pattern — shipping it on a podcast-adjacent site reads as 'category-standard UX' to visitors.15 mouse-aware 3D tilt hover cards — e-commerce product spotlight, glassmorphism parallax team card, interactive pricing tier, holographic NFT collectible, pop-out mascot, dark tech grid with border glow, media player album art, blog article preview, cyberpunk neon glow, dashboard KPI widget, minimalist real estate, flip-to-back tilt, mobile app showcase, course learning card, and a Pure CSS touch-friendly tilt. Vanilla JS writes --rx/--ry/--mx/--my; all rendering stays in CSS on the GPU.
24 hand-coded CSS animated card patterns organised by what triggers the motion, not by card style. Scroll and entrance triggers: staggered grid reveals, @starting-style mount-in, scroll-driven scale and fade on animation-timeline: view(), View Transitions API card-to-detail morphs, blur-to-sharp lazy image loading, and FLIP re-layout when a filter changes. State-change triggers: add-to-cart success morph, like and save particle burst, expand and collapse to intrinsic height with calc-size(), swipe-to-dismiss on Pointer Events, and skeleton-to-loaded crossfade. Data-driven triggers: count-up KPI metrics, SVG sparkline draw-on-enter, and progress ring fill on load. Ambient idle loops: breathing glow, floating drift, animated gradient mesh backgrounds, conic rotating borders, auto-cycling testimonial decks, and live status pulses. Plus four hover patterns where the effect is the topic itself: cursor spotlight, holographic foil glare, corner ribbon slide, and horizontal accordion expand. Every card is scoped under a .ac-NN prefix for no-collision pasting, guards prefers-reduced-motion, animates compositor-only properties, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.
22 hand-coded CSS avatars for chat apps, team dashboards, comment threads, account menus, and social profiles. Covers circular and squircle shapes, gradient and conic story rings, hexagon clip-path crops, online status dots, notification count badges, verified checkmarks, stacked facepiles with plus-N overflow, initials fallbacks for users without photos, broken-image recovery, avatar pickers, and a size scale system where the ring, badge, and status dot all scale from a single custom property.