Works with the AI Agents you already use

    Claude CodeClaude CodeCursorCursorCodex CLICodex CLIMuseMuseDotsDots+22 more

    Building Frontend UI — A11y, Tokens & Anti-Vibe Audit

    1

    Build and review UI in any framework: reuse first, accessible by default, every state handled, no AI-generated look.

    Free

    Security scanned
    Building Frontend UI — A11y, Tokens & Anti-Vibe Audit

    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

    PriorityIssueFix
    1. AccessibilityClickable <div> and <span> used as buttons. Neither can be focused or used from the keyboardUse Button (a real <button>)
    1. AccessibilityThe 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 roleDialog already does all of this. Drop the custom close span
    2. SecuritydangerouslySetInnerHTML with projectName. A project named <img src=x onerror=…> would run scriptRender the name as React text through Trans components. No HTML injection
    3. Reuse & tokensParallel modal and button implementations, raw hex, an off-scale 13px padding, an inline radiusUse Dialog, Button and token classes on the 4/8 scale (mt-6, gap-2)
    4. Component designisDanger 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 designBug: the backdrop's onClick also fires on clicks inside the panel, because events bubbleFixed by using Dialog. No hand-written backdrop
    6. Forms & feedbackLoading shows "..." and the confirm button can still be clicked, so a double submit is possibleButton 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, Button and 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 projectName wraps 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 en and es.
    • Tests written and passing. Not run yet (see above).

    Next steps

    1. Update the call sites to the new name and props.
    2. Run the tests above, then check the open state at 375px to confirm the buttons stay at least 44px tall and don't overflow.
    3. If Dialog doesn't move focus to the least destructive action (Cancel) when it opens, raise it as a Dialog change, not as a local workaround.

    delete-project-dialog.tsx

    TSX · React component

    Generated

    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 localStorage and 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

    Listed11 days ago

    What's inside

    Frequently Asked Questions