Shared Svelte components and utilities for Figma plugins.
npm install figma-plugin-utilities
import {
// Components
PluginLayout,
Header,
Footer,
StatusBar,
EmptyState,
ListItem,
LoadingState,
FieldGroup,
CheckboxCard,
// Messages
sendToPlugin,
createMessageHandler,
// Colors
rgbToHex,
hexToRgb,
getLuminance,
getContrastRatio,
meetsContrastLevel,
// Validation
validateUrl,
validateJsonString,
validateEmail,
validateNumber,
sanitizeName,
sanitizeInput,
isEmpty,
// Error handling
safeAsync,
parseJsonSafe,
notifyError,
notifySuccess,
notifyWarning,
// Resize
setDefaultWidth,
getContentHeight,
resizeToFit,
autoResize,
} from "figma-plugin-utilities";
// Components only
import { PluginLayout, Header, Footer } from "figma-plugin-utilities/components";
// Utilities only
import { sendToPlugin, createMessageHandler } from "figma-plugin-utilities/lib";
| Component | Description |
|---|---|
PluginLayout |
Main content wrapper with scrollable area |
Header |
Header bar with left, center, right slots and optional title |
Footer |
Footer with right, split, and full layout variants |
StatusBar |
Toast notifications with auto-dismiss (info/success/error/warning) |
EmptyState |
Empty/error states with optional icon and action buttons |
ListItem |
Selectable list items with metadata slot and action menu |
LoadingState |
Centered loading indicator with custom message |
FieldGroup |
Label + input wrapper for form fields |
CheckboxCard |
Large checkbox with card styling and better touch targets |
<Header title="My Plugin">
<svelte:fragment slot="left">
<IconButton iconName={IconBack} on:click={goBack} />
</svelte:fragment>
<svelte:fragment slot="right">
<IconButton iconName={IconSettings} />
</svelte:fragment>
</Header>
<!-- Without border -->
<Header title="Settings" noBorder />
<!-- Right-aligned (default) -->
<Footer>
<Button variant="primary">Save</Button>
</Footer>
<!-- Split layout -->
<Footer variant="split">
<svelte:fragment slot="left">
<Button variant="secondary">Cancel</Button>
</svelte:fragment>
<svelte:fragment slot="right">
<Button variant="primary">Save</Button>
</svelte:fragment>
</Footer>
<!-- Full-width buttons -->
<Footer variant="full">
<Button variant="primary">Generate</Button>
</Footer>
<StatusBar
message={status.message}
type={status.type}
on:close={() => status = { message: '', type: 'info' }}
/>
Types: info, success, error, warning. Auto-dismisses after 4s for info and success.
<EmptyState
message="No items yet"
icon="search"
actions={[
{ label: "Add Item", handler: handleAdd },
{ label: "Import", handler: handleImport }
]}
/>
<ListItem
id="item-1"
title="My Item"
active={selectedId === 'item-1'}
menuItems={[
{ label: 'Edit', value: 'edit' },
{ label: 'Delete', value: 'delete' }
]}
on:click={handleSelect}
on:menuSelect={handleMenuAction}
>
<span>Additional metadata</span>
</ListItem>
Large checkbox with card-style background and better touch targets.
<!-- Basic usage -->
<CheckboxCard
checked={isSelected}
on:change={handleToggle}
>
Small
</CheckboxCard>
<!-- With secondary text -->
<CheckboxCard
checked={isSelected}
on:change={handleToggle}
>
Small
<svelte:fragment slot="secondary">400px</svelte:fragment>
</CheckboxCard>
<!-- Disabled state -->
<CheckboxCard
checked={true}
disabled={true}
>
Large
</CheckboxCard>
lib/messages.js)// Send message to plugin code
sendToPlugin("my-action", { data: "value" });
// Handle messages from plugin
window.onmessage = createMessageHandler({
success: (msg) => console.log("Success:", msg),
error: (msg) => console.error("Error:", msg),
});
lib/colors.js)// Convert between formats (Figma uses 0-1 range)
const rgb = hexToRgb("#FF0000"); // { r: 1, g: 0, b: 0 }
const hex = rgbToHex({ r: 1, g: 0, b: 0 }); // "#FF0000"
// Contrast utilities
const luminance = getLuminance({ r: 1, g: 0, b: 0 });
const ratio = getContrastRatio(color1, color2);
const passes = meetsContrastLevel(ratio, "AA"); // true/false
lib/validation.js)const urlResult = validateUrl("https://example.com");
// { valid: true } or { valid: false, error: "..." }
const jsonResult = validateJsonString('{"key": "value"}');
// { valid: true, parsed: {...} } or { valid: false, error: "..." }
validateEmail("[email protected]"); // { valid: true }
validateNumber("42", { min: 0, max: 100 }); // { valid: true, value: 42 }
const clean = sanitizeName("My Plugin!!!"); // "My Plugin"
sanitizeInput("<script>alert(1)</script>"); // escaped string
isEmpty(""); // true
lib/errorHandling.js)// Safe async operations
const result = await safeAsync(
() => fetch(url),
"Loading data"
);
if (result.ok) {
console.log(result.value);
} else {
console.error(result.error.userMessage);
}
// Parse JSON safely
const parsed = parseJsonSafe(jsonString);
// { ok: true, value: {...} } or { ok: false, error: "..." }
// Figma notifications
notifySuccess("Done!");
notifyError("Something went wrong");
notifyWarning("Check your input");
lib/resize.js)Utilities for dynamically resizing the plugin window to fit its content.
// One-time resize to fit content
resizeToFit({ width: 300, minHeight: 100, maxHeight: 600 });
// Watch for content changes and auto-resize
const cleanup = autoResize({
container: myContainerEl, // bind:this on a naturally-flowing wrapper
width: 300,
minHeight: 100,
maxHeight: 600,
});
// Call cleanup when the component is destroyed
onDestroy(cleanup);
// Set default width used across all resize calls
setDefaultWidth(320);
Note: The
containerelement passed toautoResizemust not haveheight: 100%or a fixed height — it should flow naturally with its content soscrollHeightcan be measured accurately.
lib/figma-helpers.ts)For use in code.ts:
import {
sendToUI,
showError,
showSuccess,
getCollections,
getVariables,
getSelection,
focusNodes,
loadFont,
saveToStorage,
loadFromStorage,
handleResize,
} from "figma-plugin-utilities/lib/figma-helpers";
// Send message to UI
sendToUI("success", { message: "Done!" });
// Show notifications
showError("Something went wrong");
showSuccess("Created!");
// Variables
const collections = await getCollections();
const colorVars = await getVariables("COLOR");
// Selection
const selected = getSelection(); // all selected nodes
const frames = getSelection("FRAME"); // filtered by type
// Focus viewport on nodes
focusNodes(figma.currentPage.selection);
// Load font before using
await loadFont("Inter", "Regular");
// Client storage
await saveToStorage("settings", { theme: "dark" });
const settings = await loadFromStorage("settings", { theme: "light" });
// Handle resize message from UI (call in your message handler)
if (msg.type === "resize") handleResize(msg);