---
name: basic-ux-figma
description: Review a Figma design against BASIC UX for people and AI agents, then annotate it so the build comes out agent-ready. Checks the selected frames for contrast, text size, tap targets, spacing and use of styles and variables (the people half), and marks what an AI agent will need in code (control names, heading levels, landmarks, field labels, alt text, status messages, actions that cannot be undone) as native Figma annotations that carry through Dev Mode and the Figma MCP server to coding agents. Use when asked to "BASIC UX review", review a design for UX or accessibility, check whether a design is agent-ready or agent-friendly, or prepare a handoff for Claude Code, Codex, Cursor or another coding agent.
license: MIT
compatibility: Figma's agent and Figma Make (upload this one file), or a coding agent with the Figma MCP server and its figma-use skill (use_figma).
---

# BASIC UX review for Figma

BASIC UX (basicux.com) is five principles, Beauty, Accessibility, Simplicity,
Intuitiveness and Consistency, asked of two users: people, who see the screen,
and AI agents, which read the page's accessibility tree and HTML. A design file
can be checked for the people half directly. The agent half is decided in code,
so this skill writes it down where the developer and their coding agent will
read it: as Figma annotations.

Two outputs, nothing else:

1. A review in chat: ten scores (each principle for people and for agents) and
   the top fixes, each tied to a layer.
2. Annotations on the frames, in two categories, "BASIC UX: people" (fixes for
   the design) and "BASIC UX: agents" (what the code must do).

Do not restyle or move anything unless the user asks. A review that rewrites
the design is not a review.

## Before you start

- Work on the selected frames. If nothing is selected, ask which frames.
- With the Figma MCP server: load the `figma-use` skill before any `use_figma`
  call, and pass `figma-use,basic-ux-figma` in `skillNames`. Read first, write
  second, and return every node ID you touch.
- In Figma's own agent, do the same steps with the tools it gives you.
- Colours in the Plugin API are 0 to 1, not 0 to 255.

## Step 1: read the frames (read only)

Run this once. It returns what the checks need and changes nothing.

```js
const sel = figma.currentPage.selection.filter((n) => "findAll" in n);
if (!sel.length) return { error: "Select one or more frames first." };
const lum = (c) => { const f = (v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)); return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b); };
const ratio = (a, b) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); };
const solid = (n) => { const f = "fills" in n && Array.isArray(n.fills) ? n.fills.filter((p) => p.visible !== false && p.type === "SOLID" && (p.opacity ?? 1) > 0.5) : []; return f.length ? f[f.length - 1].color : null; };
const bgOf = (n) => { let p = n.parent; while (p && p.type !== "PAGE") { const c = solid(p); if (c) return c; p = p.parent; } return { r: 1, g: 1, b: 1 }; };
const CONTROL = /button|btn|link|input|field|select|dropdown|checkbox|radio|toggle|switch|tab|search|cta|icon/i;
const out = { frames: [], text: [], controls: [], images: [], spacing: {}, fonts: {}, hardcoded: { fills: 0, text: 0 } };
for (const root of sel) {
  out.frames.push({ id: root.id, name: root.name, width: Math.round(root.width), height: Math.round(root.height) });
  for (const n of [root, ...root.findAll(() => true)]) {
    if (n.visible === false) continue;
    if (n.type === "TEXT") {
      const fg = solid(n), size = typeof n.fontSize === "number" ? n.fontSize : null;
      const bold = typeof n.fontWeight === "number" && n.fontWeight >= 700;
      const family = typeof n.fontName === "object" ? n.fontName.family : "mixed";
      out.fonts[family] = (out.fonts[family] || 0) + 1;
      out.text.push({ id: n.id, text: n.characters.slice(0, 60), size, contrast: fg ? Math.round(ratio(fg, bgOf(n)) * 100) / 100 : null, needs: size && (size >= 24 || (size >= 18.66 && bold)) ? 3 : 4.5, styled: typeof n.textStyleId === "string" && n.textStyleId !== "" });
      if (!(typeof n.textStyleId === "string" && n.textStyleId)) out.hardcoded.text++;
    } else if ("fills" in n && Array.isArray(n.fills) && n.fills.some((p) => p.type === "SOLID" || p.type.startsWith("GRADIENT")) && !n.fillStyleId && !(n.boundVariables && n.boundVariables.fills)) {
      out.hardcoded.fills++;
    }
    if (n !== root && (n.type === "INSTANCE" || n.type === "COMPONENT" || n.type === "FRAME") && CONTROL.test(n.name)) {
      const words = n.findAll((c) => c.type === "TEXT").map((t) => t.characters).join(" ").trim();
      out.controls.push({ id: n.id, name: n.name, width: Math.round(n.width), height: Math.round(n.height), text: words.slice(0, 40), iconOnly: !words });
    }
    if ("fills" in n && Array.isArray(n.fills) && n.fills.some((p) => p.type === "IMAGE")) out.images.push({ id: n.id, name: n.name });
    if ("layoutMode" in n && n.layoutMode !== "NONE") for (const v of [n.itemSpacing, n.paddingTop, n.paddingLeft]) if (v > 0) out.spacing[v] = (out.spacing[v] || 0) + 1;
  }
}
out.text = out.text.slice(0, 400);
out.controls = out.controls.slice(0, 200);
return out;
```

Then take one screenshot of each frame (`get_screenshot`, or look at the
canvas) for the checks a script cannot do.

## Step 2: check the people half

Score each principle from 100, minus 10 for each high finding, 5 for each
medium and 2 for each low. Cite the number for every finding.

- **Beauty.** Is it aesthetically pleasing? From the screenshot: one focal
  point, clear hierarchy, consistent alignment, no generic AI-design tells
  (purple-to-blue gradient hero, badge over a centered H1 with two buttons,
  emoji as icons, the same card grid everywhere). More than two font families
  in `fonts` is medium.
- **Accessibility.** Can everyone use it? Text under its contrast `needs`
  value is high. Body text under 16px is medium (12 to 13px is fine for
  labels and captions). A control under 24 by 24 is high; under 44 by 44 on a
  mobile frame is medium. Meaning carried by colour alone is medium. Text a
  reader with ADHD, dyslexia or little schooling would struggle with is
  medium: judge the text layers from step 1 against a 6th grade reading
  level.
- **Simplicity.** Does it make life easier? More than one primary action per
  screen, or more than about twelve choices in the first screen, is medium.
  Repeated or filler copy is low. A sentence over 25 words, or a text block
  over 3 sentences, is low each.
- **Intuitiveness.** Is it easy to learn? Unfamiliar patterns, links that do
  not look like links, buttons that do not look pressable, and a form with no
  error, empty or success state designed are each medium. A heading over 8
  words, an idiom, or a term used without explaining it is low each.
- **Consistency.** Does it match the system? Spacing values off a 4 or 8
  scale (see `spacing`) are low each. Hard-coded fills and text (see
  `hardcoded`) instead of styles or variables are medium per dozen. The same
  component styled two ways is medium.

## Step 3: check the agent half

An agent will read the built page's accessibility tree. Check that the design
says enough for the code to get it right. Score the same way.

- **Beauty: is it well formed?** One H1 per screen and heading levels that do
  not skip. The visual reading order matches a sensible source order.
- **Accessibility: can an agent read and use it?** Every icon-only control
  (`iconOnly: true`) needs a name. Every input needs a visible label, not just
  a placeholder. Every meaningful image needs alt text.
- **Simplicity: does it take the fewest steps?** The main task (book, buy,
  search, contact) can be done in one form on one screen where possible.
- **Intuitiveness: does it say what it does?** Link and button text names
  the destination or action, never "Learn more" or "Click here" alone. Forms
  have a designed status message and error message. Actions that cannot be
  undone (delete, pay, send) have a confirm step and say so in the label.
- **Consistency: does it act the same every time?** The same label always
  means the same action or destination across frames. What the screen says
  matches what an agent would be told: no hidden text that differs.

## Step 4: annotate

Use native annotations so they show in Dev Mode and reach coding agents
through the Figma MCP server. One annotation per finding, short, in this
fixed vocabulary so a coding agent can map each to code:

| For | Annotation text |
| --- | --- |
| Icon-only control | `**Name:** Close dialog` |
| Heading | `**Heading:** level 2` |
| Region | `**Landmark:** navigation "Primary"` (header, navigation, main, footer) |
| Field | `**Label:** Email · type=email · autocomplete=email` |
| Image | `**Alt:** Mechanic truing a wheel` or `**Alt:** decorative` |
| Form result | `**Status:** role=status, empty until submit, then "Request sent. We reply within a day."` |
| Form error | `**Error:** role=alert, tied to the field` |
| Irreversible action | `**Consequence:** cannot be undone · confirm step · label "Delete permanently"` |
| Vague link | `**Link text:** "See tune-up prices", goes to /services` |
| People fix | `**Contrast:** 3.1:1, needs 4.5:1` or `**Target:** 20×20, needs 24` |
| Hard text | `**Plain text:** 38 words in one sentence; split it, aim for 15` |

Agent items go in the "BASIC UX: agents" category, people fixes in "BASIC UX:
people". Re-running the review replaces this skill's annotations and leaves
everyone else's alone:

```js
const cats = await figma.annotations.getAnnotationCategoriesAsync();
const agents = cats.find((c) => c.label === "BASIC UX: agents") || (await figma.annotations.addAnnotationCategoryAsync({ label: "BASIC UX: agents", color: "blue" }));
const people = cats.find((c) => c.label === "BASIC UX: people") || (await figma.annotations.addAnnotationCategoryAsync({ label: "BASIC UX: people", color: "orange" }));
const ours = new Set([agents.id, people.id]);
// one entry per node: the IDs come from step 1
const notes = [
  { id: "12:34", agent: true, md: "**Name:** Close dialog" },
  { id: "12:40", agent: false, md: "**Contrast:** 3.1:1, needs 4.5:1" }
];
const byNode = {};
for (const n of notes) (byNode[n.id] = byNode[n.id] || []).push(n);
const mutatedNodeIds = [], failed = [];
for (const [id, list] of Object.entries(byNode)) {
  const node = await figma.getNodeByIdAsync(id);
  if (!node || !("annotations" in node)) { failed.push(id); continue; }
  try {
    const kept = node.annotations.filter((a) => !ours.has(a.categoryId));
    node.annotations = [...kept, ...list.map((n) => ({ labelMarkdown: n.md, categoryId: (n.agent ? agents : people).id }))];
    mutatedNodeIds.push(id);
  } catch (e) { failed.push(`${id}: ${e.message}`); }
}
return { mutatedNodeIds, failed };
```

If a node will not take an annotation (a layer deep inside an instance, for
example), annotate the nearest parent that will and name the child in the text.

## Step 5: report

In chat, give the ten scores as a small table (principle, people, agents), then
the top five fixes ordered by impact, each with the layer name and the number
behind it. Say how many annotations you added in each category. If the user
wants the summary on the canvas, place a frame named "BASIC UX review" to the
right of the selection, built with auto layout.

## After the build

The annotations are the brief for the code. The basic-ux-design skill (build
rules) and basic-ux-audit skill (scores the live page for people and agents)
pick up from here: basicux.com/skills.html, or `npx skills add
https://basicux.com`.
