ux-writing
Write the interface copy for a flow or component — buttons, labels, hints, errors, empty states, confirmations, loading and success messages — as a string table where each string has its context, a character budget, a recommended version and one variant, with error messages that say what happened and what to do next, and a terminology list so the same thing has the same name everywhere. Use when the user wants UI copy, microcopy, button text, error messages, empty states, "write the strings for this screen", or "our wording is inconsistent".
Other ways to install
With the sibling skills it hands off to:
The whole pack it belongs to:
By hand, from a clone of the repo:
In Claude Code, with the experience-design plugin installed, invoke it as:
Try it
A realistic prompt from the skill's acceptance evals:
Write the UI copy for Acme Bank's 'add a payee' flow in our mobile app. Steps: enter payee name, sort code and account number; we run a name check against the bank's records (match, close match, no match); then confirm and add. Users can also see an empty Payees list before they've added anyone. Our style guide says: plain, calm, second person, no exclamation marks.
What a good result looks like
Example output
the copy for a team invite flow · the skill's own worked example, source
We're adding "invite teammates" to Northwind Boards (project management SaaS). Flow: Team page → Invite button → modal with an email field (multiple, comma separated) and a role picker (Admin / Member) → send. There's also a Pending invites tab where you can revoke an invite. Right now the modal says "Add users" and the button says "Submit", and the Team page calls them "members" while the billing page says "seats". Voice: friendly but not cute. Web only, English only for now.
Interface copy: team invite flow
Voice: friendly but not cute (given) → plain, "you", sentence case, no exclamation marks Platform: web · Localised: no, so budgets have no growth allowance Assumptions to confirm first:
- There is a per-send invite limit (
{limit}below, value not given). - Admins can manage billing and members, and Members can't. The role hint depends on this, so confirm it before shipping.
- Invites go out by email with a join link, and revoking one kills the link without emailing the invitee.
Character counts include spaces and count each {placeholder} as written. Leave room for the real value: an email address can run 30+ characters.
String table
| ID | Screen | Slot | Context | Budget | Recommended (count) | Variant (count, why) |
|---|---|---|---|---|---|---|
invite.title | Invite | page title | Opens from Team › Invite | 40 | "Invite people to your workspace" (31) | "Invite teammates" (16, shorter) |
invite.email.label | Invite | label | Email field | 30 | "Email addresses" (15) | "Emails" (6, shorter) |
invite.email.hint | Invite | hint | Under the email field | 80 | "Separate addresses with commas. You can invite up to {limit} at once." (69) | "Add one or more emails, separated by commas." (44, shorter; drops the limit) |
invite.role.label | Invite | label | Role picker | 30 | "Role" (4) | "Their role" (10, warmer) |
invite.role.hint | Invite | hint | Under the role picker | 80 | "Admins can manage billing and members. Members can use the workspace." (69) | "Choose what they can do in this workspace." (42, shorter; less explicit) |
invite.submit | Invite | button | Primary action | 20 | "Send invites" (12) | "Invite" (6, shorter) |
invite.error.email_format | Invite | error | An address isn't a valid email | 100 | "“{value}” isn't an email address. Check it for typos, like a missing @." (71) | "Check “{value}”. It needs an @ and a domain, like sam@northwind.example." (72, more explicit) |
invite.error.already_member | Invite | error | Address belongs to an existing member | 100 | "{email} is already in this workspace. Remove it to send the other invites." (74) | "{email} is already a member, so we won't invite them again." (59, warmer; no action) |
invite.error.limit | Invite | error | More addresses than the limit | 100 | "You can invite up to {limit} people at once. Remove {excess} to continue." (73) | "That's more than {limit}. Send these in two batches." (52, shorter) |
invite.error.network | Invite | error | Request failed or timed out | 100 | "We couldn't send the invites. Check your connection and try again." (66) | "Invites not sent. Try again in a moment." (40, shorter) |
invite.loading | Invite | loading | After pressing Send invites | 40 | "Sending invites…" (16) | "Sending {n} invites…" (20, more explicit) |
invite.success | Invite | toast | All invites sent | 60 | "Invites sent to {n} people" (26) | "{n} invites sent. They'll get an email." (39, more explicit) |
pending.empty.title | Pending invites | empty | No pending invites | 40 | "No pending invites" (18) | "Nobody's waiting to join" (24, warmer) |
pending.empty.body | Pending invites | empty | Under the empty title | 100 | "People you invite show here until they accept." (46) | "Invites you send will appear here." (34, shorter) |
pending.empty.action | Pending invites | button | Empty state action | 20 | "Invite people" (13) | "Send an invite" (14, warmer) |
pending.revoke.title | Pending invites | dialog title | Revoke an invite | 40 | "Revoke invite for {email}?" (26) | "Revoke this invite?" (19, shorter; drops the name) |
pending.revoke.body | Pending invites | dialog body | Revoke dialog | 160 | "The link in their email will stop working. You can invite them again later." (75) | "They won't be able to join with this invite. You can send a new one any time." (77, warmer) |
pending.revoke.confirm | Pending invites | button | Destructive confirm | 20 | "Revoke invite" (13) | "Revoke" (6, shorter) |
pending.revoke.cancel | Pending invites | button | Dismiss | 20 | "Keep invite" (11) | "Cancel" (6, shorter; less clear) |
Recommendations, briefly: use the recommended column throughout. "Send invites" says what the button does where "Submit" doesn't. The already-member error keeps the action ("Remove it…") because the variant leaves the user unsure whether the other invites went. "Keep invite" beats "Cancel" in the revoke dialog, because "Cancel" next to "Revoke invite" reads as "cancel the invite".
Terminology
| Concept | Use | Avoid | Notes |
|---|---|---|---|
| A person with access | member | user, seat, teammate (in UI labels) | The billing page says "seats". Change it to "members", or explain "you pay per member". "Teammates" only in the page title variant. |
| The shared space | workspace | team, account, organisation | The Team page title already says "Team". Rename it to "Members", or flag it for information-architecture. |
| Asking someone to join | invite (verb and noun) | add, add users | Replaces "Add users" in the current modal |
| Withdrawing an invite | revoke | cancel, delete | Keeps "Cancel" free to mean "close this dialog" |
| Permission level | role | permission, access level | Role names: Admin, Member |
Open questions
- What is
{limit}, the most invites per send? The hint and the limit error need it. - Does an invite expire? If so, add an
invite.expirederror for the invitee's landing page and say the expiry in the success toast. - Can a Member invite, or only an Admin? If only Admins can, Members need a disabled-state hint on the Invite button.
- Should the billing page change from "seats" to "members"? That's a product decision, not a copy one.
How it's checked
Measured against plain Claude Code (2026-10-23): it met 81% of its checks, against 60% without it, and a blind judge preferred the agent without it. How skills are checked
21 assertions across 3 acceptance evals, and 8 trigger queries. Every skill's checks
What the output must do
When it should and shouldn't activate
Should activate:
- Write the button labels and error messages for our signup form
- We need empty states, loading and success messages for the file upload screen
- Our app calls the same thing a project, a board and a space — fix the microcopy and give us a terminology list
- Rewrite these error messages so people know what to do: 'Invalid input', 'Error 500', 'Request failed'
Should leave to another skill:
- Plan the content for our benefits page from user needs: what questions it answers and what to cut
- Write a getting-started guide for new admins setting up the product
- Set up our brand kit from this logo and our tone-of-voice guidelines
- Audit this checkout page for WCAG 2.2 AA issues
Hands off to
- accessibility-audit — when the strings are built into a working UI. Check the built flow against WCAG, which a string table can't prove.
Related skills
brand-kit, content-design, design-system-spec, guide-builder
Source
SKILL.md · the whole folder on GitHub.