Live Session PillA compact live-session presence pill with an animated speaking indicator and overlapping avatars — clicks open a soft-UI modal showing all participants with a Join Now button.
UICollaboration toolSaaSGame UI
Switch to light
Refresh
Full screen
Loading source…
Add to your project
One command adds this component to your project.
1
Run the following command. New project? Run npx shadcn@latest init first to set up Tailwind and path aliases.
npx shadcn@latest add @aicanvas/voice-chat-pill
2
For dark mode, add the dark class to your <html> element:
Optional
<html class="dark">
Install with AI Canvas MCP
With AI Canvas MCP, your AI knows every component we ship. Ask for one inside Claude Code, Codex, or Cursor and it installs the component you pick. Works with any AI Canvas account, free or premium.
Live Session Pill is a compact presence indicator with an animated speaking pulse and overlapping participant avatars, that opens into a soft-UI modal when tapped — showing every participant with a Join Now button. Motion drives both the speaking pulse and the pill-to-modal expansion. It slots into collaboration tools, multiplayer game lobbies, design-tool comment threads, and any product where live presence deserves a compact, glanceable affordance with a fast path to joining the conversation.
Built with
MotionTailwind CSS
Frequently asked questions
Is Live Session Pill free to use?
Yes. Live Session Pill is part of the free AI Canvas library and is open source under the MIT license, so you can use it in personal and commercial projects.
How do I install Live Session Pill?
One command: npx shadcn@latest add https://aicanvas.me/r/voice-chat-pill.json, free with a free AI Canvas account. Or connect the AI Canvas MCP server and ask your AI editor to install Live Session Pill for you. You can also copy the source straight from the Code tab, no account needed. It works in any React project with Tailwind CSS.
What is Live Session Pill built with?
Live Session Pill is built with React and TypeScript, using Motion and Tailwind CSS. It ships with both light and dark styling.
Where would I use Live Session Pill?
Common uses include Collaboration tool, SaaS, and Game UI. Like every AI Canvas component, it is self-contained and drops into any React project.
Can I remix Live Session Pill with AI?
Yes. Live Session Pill ships with one comprehensive AI prompt written against the real source code. Open "Remix with AI" on this page to read and copy it into Claude, Cursor, ChatGPT, or any AI tool. Prompts are for remixing your own variation; for the exact component, install it with the one-command CLI.
One comprehensive prompt, written against the real source code. Works in Claude, Cursor, ChatGPT, or any AI tool you use.
This prompt is for remixing. Use it to build your own variation of Live Session Pill. Results depend on the model you use, and no prompt in the world is 100% exact.
Want the exact component?
One command installs it, pixel-perfect. Copy, paste into your project, done.
npx shadcn@latest add @aicanvas/voice-chat-pill
AI prompt for Live Session Pill
Before building, verify your project has the following setup:
- React / Next.js
- TypeScript
- Tailwind CSS v4
If any are missing, set up via the shadcn CLI:
npx shadcn@latest init
---
Create `components-workspace/voice-chat-pill/index.tsx`. Export a default function `VoiceChatPill`.
**Container:** `flex min-h-screen w-full items-center justify-center bg-[#E8E8DF] dark:bg-[#1A1A19]`.
---
## Collapsed pill
A `motion.button` ref'd as `pillRef`, pill-shaped (rounded-full), no bottom inset border.
**Colors (inverted per theme — same pattern as new-project-modal):**
- Light theme: bg `#1a1a18`, hover `#2d2d2b`
- Dark theme: `dark:bg-[#e0dfd8]`, `dark:hover:bg-[#d4d3cc]`
**Box shadow (pill):** `0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)` — subtle drop, no inset border.
**Hover/tap:** `whileHover={{ scale: 1.04 }}`, `whileTap={{ scale: 0.96 }}`, spring stiffness 400 damping 28. When modal is open, button fades out (opacity 0, scale 0.85, duration 0.18) and disables pointer events.
**On click:** capture `pillRef.getBoundingClientRect()` into `origin` state, then `setOpen(true)`.
**Pill contents (left → right):**
1. **Speaking icon** — 36×36 rounded-full circle:
- Light theme: bg `#f1f1f0`, icon color `#1a1a18`
- Dark theme: `dark:bg-[#1a1a18]`, `dark:text-[#f1f1f0]`
- Contains 4 animated vertical bars (`SpeakingBars` component, size 14).
2. **Avatar stack** — 4 overlapping circles (32×32), negative margin `-10px`, z-index decreasing. Each wrapped in `motion.span` that animates `y: -1.5, scale: 1.06` when this avatar is the current speaker (spring 380/24). Ring: `ring-2 ring-[#1a1a18] dark:ring-[#e0dfd8]` (matches pill bg per theme).
3. **Count + chevron** — `+{hiddenCount}` and `<CaretDown weight="bold" size={13} />` in a flex span. Color: `text-[#f1f1f0] dark:text-[#1a1a18]`.
---
## SpeakingBars component
4 bars side by side (`gap 1.5px`), each `width: 2px`, `height: size * 0.85`. Each bar:
- `motion.span` with `className="origin-center rounded-full bg-current"`
- `animate={{ scaleY: [0.35, 1, 0.55, 0.9, 0.35] }}`
- `transition={{ duration: 1.05, repeat: Infinity, delay: i * 0.13, ease: 'easeInOut' }}`
---
## Auto-rotating speaker
`useEffect` with `setInterval` — every 2400ms, advance `speakerId` by 1 through all 8 participants. Both the collapsed pill (avatar lift) and the modal grid badge stay in sync.
---
## Participants (8 total)
```ts
const PEOPLE = [
{ id: 0, name: 'David', avatar: 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?w=240&h=240&fit=crop&q=80' },
{ id: 1, name: 'Kira', avatar: 'https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=240&h=240&fit=crop&q=80' },
{ id: 2, name: 'Marina', avatar: 'https://images.unsplash.com/photo-1554780336-390462301acf?w=240&h=240&fit=crop&q=80' },
{ id: 3, name: 'Razvan', avatar: 'https://images.unsplash.com/photo-1603415526960-f7e0328c63b1?w=240&h=240&fit=crop&q=80' },
{ id: 4, name: 'Ana', avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=240&h=240&fit=crop&q=80' },
{ id: 5, name: 'Daniel', avatar: 'https://images.unsplash.com/photo-1654110455429-cf322b40a906?w=240&h=240&fit=crop&q=80' },
{ id: 6, name: 'Afshin', avatar: 'https://images.unsplash.com/photo-1639747280804-dd2d6b3d88ac?w=240&h=240&fit=crop&q=80' },
{ id: 7, name: 'Lina', avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=240&h=240&fit=crop&q=80' },
] as const
```
Pill shows first 4, `hiddenCount = 4` (displayed as `+4`).
---
## Backdrop
Fixed inset-0 div, z-40, `bg-black/25 backdrop-blur-[3px]`. Fade in/out 0.22s. Click → `close()`.
---
## Modal — pill-to-card morph
Centered fixed div z-50. `motion.div` morphs from pill origin:
- Compute: `targetW = Math.min(440, window.innerWidth - 32)`, `initialOffsetX`, `initialOffsetY`, `initialScaleX = origin.w / targetW`.
- `initial`: x:offset, y:offset, scaleX:initialScaleX, scaleY:0.18, borderRadius:9999, opacity:0.85, boxShadow.
- `animate`: all zeroed, scaleX:1, scaleY:1, borderRadius:28, opacity:1.
- `exit`: reverses to initial but opacity:0.
- `transition`: `MORPH = { type:'spring', stiffness:320, damping:30, mass:1 }` for default; borderRadius: duration 0.32 ease [0.32,0.72,0.34,1]; opacity: 0.18.
- **Box shadow (modal):** `0px 16px 56px rgba(0,0,0,0.14)` — soft ambient, no inset border.
- Card: `bg-[#f1f1f0]`, max-width 440px, padding px-6 pb-6 pt-6, rounded-[28px].
**Inner stagger:** `motion.div` with hidden/show/exit variants, staggerChildren 0.05, show delay 0.18.
**Header:** "Live Session" (18px, bold, `#1a1a18`) centered. Close × button absolute right: size-9 rounded-full, bg `#f8f8f8`, hover `#ececec` (spring 400/26), text `#6c6c6c`.
**Avatar grid:** `grid-cols-4 gap-x-3 gap-y-4`. Each cell: avatar (60×60, rounded-full, `motion.div` scale 1.04 when speaking) + name (13px, weight 500, `#1a1a18`). Active speaker shows a badge (`AnimatePresence`) top-right of avatar: 22×22 dark circle (`#1a1a18`) with `SpeakingBars size={10}` in light color, spring pop-in/out.
**Join Now button:** Full-width rounded-full, `bg-[#1a1a18]`, hover `#2d2d2b`, text `#f2f1ec` (15px bold). Has a `joining` state: on click → `setJoining(true)`, button scale 0.96, bg `#3a3a38`, label swaps to animated `Check` icon (size 18, spring 400/22 in/out via AnimatePresence mode:wait). After 700ms → `close()` and `setJoining(false)`.
---
## Dependencies
- `framer-motion` (motion, AnimatePresence)
- `@phosphor-icons/react` (CaretDown, X, Check)
## Typography
- Font: project default sans-serif (Manrope)
- Sizes: 13, 14, 15, 18 px
- Weights: 500, 600, 700