- Home
- Skills
- Frontend & Web Apps
- Building Frontend UI — A11y, Tokens & Anti-Vibe Audit
Works with the AI Agents you already use
Building Frontend UI — A11y, Tokens & Anti-Vibe Audit
Build and review UI in any framework: reuse first, accessible by default, every state handled, no AI-generated look.
Free
Building Frontend UI — A11y, Tokens & Anti-Vibe Audit
Example session with this skill installed
Can you clean up this modal? It's in our React + TypeScript app. Context: we already have Dialog (src/components/ui/dialog.tsx, wraps native <dialog>, props open, onClose, title, handles Esc and focus) and Button (src/components/ui/button.tsx, variant: "primary" | "secondary" | "danger", loading?: boolean). Tailwind with our tokens in tailwind.config.ts (text-fg, text-fg-muted, bg-surface, text-danger). i18n is react-i18next with src/locales/en/common.json and src/locales/es/common.json.
export function DeleteProjectModal({ open, onClose, onConfirm, isDanger, isLoading, isSmall, projectName }) {
if (!open) return null;
return (
<div className="fixed inset-0 bg-black/50" onClick={onClose}>
<div style={{ background: "#fff", padding: 13, borderRadius: 10 }}>
<h3 style={{ color: "#E11D48" }}>Are you sure?</h3>
<p dangerouslySetInnerHTML={{ __html: `Delete <b>${projectName}</b>? This can't be undone...` }} />
<div className="btn" onClick={onConfirm}>{isLoading ? "..." : "Delete ✨"}</div>
<span onClick={onClose} style={{ cursor: "pointer" }}>x</span>
</div>
</div>
);
}
- Read your context and instructions
- Compiled the building frontend ui
- Generated the UI component
Summary. The project already has a design system, so its tokens and components set the rules here. The fix is mostly reuse. Dialog and Button already provide the focus handling, Esc, loading state and styling that this modal rebuilds badly. Two problems are serious. projectName goes into innerHTML (an XSS risk), and any click inside the panel bubbles up to the backdrop and closes the modal.
Step 0 audit
Tokens: tailwind.config.ts (text-fg, text-fg-muted, bg-surface, text-danger). The component hardcodes #fff, #E11D48, 13px and 10px.
Reused: Dialog provides the native <dialog>, Esc, focus trap, focus restore and title. Button provides the danger variant and the loading state.
i18n: react-i18next with en and es. Every string below goes through t/Trans, and each new key is added to both locale files.
Findings, in the skill's priority order
| Priority | Issue | Fix |
|---|---|---|
| 1. Accessibility | Clickable <div> and <span> used as buttons. Neither can be focused or used from the keyboard | Use Button (a real <button>) |
| 1. Accessibility | The close control "x" has no accessible name, there is no Esc handling, focus isn't moved into or restored from the modal, and it has no dialog role | Dialog already does all of this. Drop the custom close span |
| 2. Security | dangerouslySetInnerHTML with projectName. A project named <img src=x onerror=…> would run script | Render the name as React text through Trans components. No HTML injection |
| 3. Reuse & tokens | Parallel modal and button implementations, raw hex, an off-scale 13px padding, an inline radius | Use Dialog, Button and token classes on the 4/8 scale (mt-6, gap-2) |
| 4. Component design | isDanger and isSmall are unused boolean flags. Props are untyped (implicit any) | Remove the flags. Use a typed props interface. The danger styling is the Button variant |
| 4. Component design | Bug: the backdrop's onClick also fires on clicks inside the panel, because events bubble | Fixed by using Dialog. No hand-written backdrop |
| 6. Forms & feedback | Loading shows "..." and the confirm button can still be clicked, so a double submit is possible | Button loading={isDeleting} blocks repeat clicks. Cancel is disabled while the deletion runs |
| 9. UX writing | "Are you sure?" and "Delete ✨" are vague and use emoji in the UI. ... should be … | Title "Delete project?" and button "Delete project": the action keeps the same name throughout. No emoji |
Refactored component (src/features/projects/delete-project-dialog.tsx)
import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
interface DeleteProjectDialogProps {
open: boolean;
projectName: string;
isDeleting: boolean;
onCancel: () => void;
onConfirm: () => void;
}
export function DeleteProjectDialog({
open,
projectName,
isDeleting,
onCancel,
onConfirm,
}: DeleteProjectDialogProps) {
const { t } = useTranslation("common");
return (
<Dialog open={open} onClose={onCancel} title={t("deleteProject.title")}>
<p className="text-fg-muted">
<Trans
ns="common"
i18nKey="deleteProject.body"
values={{ projectName }}
components={{ strong: <strong className="text-fg" /> }}
/>
</p>
<div className="mt-6 flex justify-end gap-2">
<Button variant="secondary" onClick={onCancel} disabled={isDeleting}>
{t("deleteProject.cancel")}
</Button>
<Button variant="danger" onClick={onConfirm} loading={isDeleting}>
{t("deleteProject.confirm")}
</Button>
</div>
</Dialog>
);
}
The component was renamed from DeleteProjectModal to DeleteProjectDialog, and its props changed: onClose became onCancel, isLoading became isDeleting, and isDanger/isSmall were removed. Update the call sites.
Locale keys (add to both files)
// src/locales/en/common.json
"deleteProject": {
"title": "Delete project?",
"body": "<strong>{{projectName}}</strong> and all its data will be permanently deleted. This can’t be undone.",
"cancel": "Cancel",
"confirm": "Delete project"
}
// src/locales/es/common.json
"deleteProject": {
"title": "¿Eliminar el proyecto?",
"body": "<strong>{{projectName}}</strong> y todos sus datos se eliminarán de forma permanente. Esta acción no se puede deshacer.",
"cancel": "Cancelar",
"confirm": "Eliminar proyecto"
}
The Spanish copy is a draft. Have a native speaker review it before release. The body text also states that the project's data is deleted. Confirm that this matches what the backend actually does.
Tests to add (delete-project-dialog.test.tsx, Testing Library + jest-axe)
it("should call onConfirm once when confirm is clicked", async () => { /* click "Delete project", expect onConfirm called 1x */ });
it("should not call onConfirm again while deleting", async () => { /* isDeleting=true, click, expect 0 calls */ });
it("should render the project name as text when it contains HTML", () => {
/* projectName='<img src=x onerror=alert(1)>' -> getByText contains the literal string, no <img> in the DOM */
});
it("should call onCancel when Escape is pressed", async () => { /* relies on Dialog's Esc handling */ });
it("should have no axe violations when open", async () => { /* expect(await axe(container)).toHaveNoViolations() */ });
These are specified but have not been run. Run them with your test command, for example npx vitest run delete-project-dialog.
Pre-delivery checklist
- Reuses
Dialog,Buttonand tokens. No hardcoded hex, px or font sizes. - Keyboard-operable with semantic buttons. Focus is handled by
Dialog, and every control has a label. - Loading state handled, with double submit blocked. A long
projectNamewraps inside the<p>. Check the truncation rule with a 200-character name. - No unsanitized HTML and no client-side secrets.
- Typed props, a single responsibility, no boolean-flag explosion.
- i18n: no hardcoded strings, and the keys exist in both
enandes. - Tests written and passing. Not run yet (see above).
Next steps
- Update the call sites to the new name and props.
- Run the tests above, then check the open state at 375px to confirm the buttons stay at least 44px tall and don't overflow.
- If
Dialogdoesn't move focus to the least destructive action (Cancel) when it opens, raise it as aDialogchange, not as a local workaround.
delete-project-dialog.tsx
TSX · React component
Example file from a real run - the skill writes it into your workspace.
Connects securely to your tools. The creator never sees your data.
About this skill
Building Frontend UI is a workflow skill for any work on the UI layer: components, pages, forms, styling, accessibility, client state, responsive layout. It works in any framework. The rules are framework-agnostic, and notes for React, Vue, Svelte and Web Components load only when the project uses them.
Problems it solves
- New components written when the project already had one that did the job.
- Raw hex colors and px values instead of design tokens, and new colors that nearly duplicate existing ones.
- Clickable
<div>s, missing labels, broken keyboard focus. - Missing loading, error and empty states.
- Hardcoded strings in an app that uses i18n.
- Tokens stored in
localStorageand unsanitized HTML. - New sites that look AI-generated: purple gradients, emoji in buttons, seven different border radii, dead clicks.
How it works. Step 0 is an audit before any code. It finds the token source, the existing components, the i18n system and the project's conventions. When concerns conflict, a ranked list decides: accessibility, then client security, then reuse, then component design, state, forms, responsive layout, performance and polish. A pre-delivery checklist closes every task.
For greenfield work there is a separate path. It produces a written design plan before any code (goal, structure, palette, type, layout), then runs a "Vibe Check" and a delivery scorecard and reports each item. When a design system already exists, the skill follows that system and does not restyle it.
References (13 files): accessibility (WCAG 2.2, APG patterns), state and data, forms, responsive layout, performance, UX writing, design craft, designing from scratch, the anti-vibe-coded audit (with a browser-console audit script), plus framework notes for React, Vue, Svelte and Web Components.
When it triggers. Whenever a component, page, form, modal, layout, CSS/Tailwind, ARIA, UX or "looks AI-generated" comes up, even if you don't say "frontend".
Free and MIT-licensed. Part of the claude-skills-czl collection.
Source on GitHub: this skill · full repository
How to use
Skills on Agensi run inside your AI agent. Copy the prompt, paste it into your agent, and it connects to Agensi and installs this skill for you. If a payment is needed, your agent shows it to you first.
Reviews
No reviews yet
Be one of the first to try it. Every listed skill passes our trust checks below.
Security scanned
Passed our 8-point scan before listing
Fresh listing
Recently published to Agensi
Free forever
No account required to browse
Trust & safety
Security scanned
Verified clean 11 days ago
- Free to download with an account