Figma Comment
Figma Comment renders a small floating comment marker that expands into a card when opened. It measures its content height after mount (and on message/width changes) to animate smoothly between collapsed (32x32) and expanded states. The avatar slides from the corner into the padded content area, and the body fades in with a subtle blur. Supports controlled/uncontrolled open state, emits openChange/update:open, and respects prefers-reduced-motion.
Usage
Place the component near a canvas element and pass author/message details. Click the marker to toggle; clicking outside closes it when open. If you want to control state externally, bind v-model:open.

<template>
<div class="flex h-40 w-full items-center justify-center">
<MFigmaComment
author-name="Arjun Mehta"
avatar-url="https://randomuser.me/api/portraits/men/11.jpg"
message="What happens if we adjust this to handle light and dark mode?"
timestamp="2 min ago"
/>
</div>
</template>
Customize

Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
authorName | string | 'Arjun Mehta' | Comment author, shown in the attribution row. |
avatarUrl | string | 'https://randomuser.me/api/portraits/men/11.jpg' | Avatar image URL. Defaults to a portrait; when absent or failing to load, avatarIcon is shown instead. |
avatarIcon | string | 'i-lucide-user-round' | Nuxt UI icon shown in the avatar slot when avatarUrl is missing or fails to load. |
avatarAlt | string | undefined | Alternative text for avatar; defaults to authorName. |
message | string | What happens if we adjust...? | Comment body text. |
timestamp | string | 'Just now' | Relative time shown next to author. |
width | number | 180 | Open card width in pixels; also used for measurement width. |
open | boolean | undefined | Controlled open state. Leave undefined for uncontrolled. |
defaultOpen | boolean | false | Initial open state for uncontrolled usage. |
class | string | undefined | Extra classes for the positioning wrapper. |
className | string | undefined | Alias of class (kept for parity with reference). |
Events
| Event | Payload | Description |
|---|---|---|
openChange | (value: boolean) => void | Emitted whenever isOpen changes (fires on mount with initial value, then on every change). Reports the actual open state after the change. |
update:open | (value: boolean) => void | v-model update emitted on toggle request. |
Slots
| Slot | Props | Description |
|---|---|---|
default | - | No default slot is used. |
UI
This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.figmaComment in app.config.ts.
| Prop | Type | Default | Description |
|---|---|---|---|
ui | object | - | Per-slot classes merged over the theme defaults. |
| Slot | Props | Description |
|---|---|---|
root | - | relative |
card | - | absolute bottom-0 left-0 cursor-pointer overflow-hidden rounded-2xl rounded-bl-none bg-default shadow-[0px_0px_0.5px_0px_rgb(0_0_0/0.18),0px_3px_8px_0px_rgb(0_0_0/0.1),0px_1px_3px_0px_rgb(0_0_0/0.1)] outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-default |
avatar | - | absolute z-10 |
avatarImage | - | size-6 rounded-full object-cover |
avatarFallback | - | flex size-6 items-center justify-center rounded-full bg-muted text-[10px] leading-none font-semibold text-inverted |
measure | - | pointer-events-none absolute |
measureInner | - | flex flex-col items-start gap-0.5 py-3 pr-4 pl-11 |
meta | - | flex items-start gap-0.5 |
author | - | text-[11px] leading-4 font-semibold text-highlighted |
timestamp | - | text-[11px] leading-4 font-medium text-muted |
message | - | text-left text-[11px] leading-4 font-medium text-highlighted |
content | - | absolute inset-0 flex flex-col items-start gap-0.5 py-3 pr-4 pl-11 |
Accessibility
The animated card container is rendered as a role="button" with tabindex="0", :aria-expanded="isOpen", and :aria-label="toggleLabel" (e.g. "Show/Hide comment from ..."). Keyboard toggles on Enter and Space (with preventDefault). A click-outside handler closes when open. Avatar has alt text or is replaced by a decorative initial span (aria-hidden="true"). The measurement element is aria-hidden="true". Focus is not programmatically moved on open/close. No prefers-reduced-motion removes focus ring semantics; reduced motion replaces animations with static positions/sizes.
Performance
- Measures content height via a hidden
measureEl(first childscrollHeight) on mount with two timeouts (100ms, 500ms) and also onmessage/widthchanges (nextTickthen measure) to ensure layout is stable. - Animations driven by
motion-v: card size animates between{height,width}(closed 32×32, opencontentHeight×width), avatar offset animatesleft/top, content animatesopacity+filter: blur(...)with delayed blur/opacity on enter. - Spring transitions: card uses
stiffness:550,damping:45,mass:0.7, avatar usesstiffness:300,damping:25, content uses customease: [0.22,1,0.36,1]for blur/opacity. useClickOutsideattached to root wrapper; click handler lives on wrapper (not directly on motion node) to ensure reliable event binding.- Avatar fallback:
avatarFailedref set on image error; also checksimg.complete && naturalWidth===0on mount/URL change. No timers beyond measurement timeouts. shouldReduceMotionreplaces all animations with static styles/zero-duration transitions and simplifies content enter to plain opacity.
scrollHeight automatically. Keep width reasonable (default 180) to avoid overly wide bubbles in tight layouts.