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
MFigmaComment exposes no slots. The avatar, author, timestamp and message are
all props.
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.