by A11yGuard (LX AI)
Contrast and ARIA in vibe-coded apps
Vibe-coded UIs fail accessibility at two points scanners rarely catch together: contrast math and accessible names. A component can pass a color audit and still be unusable when its role is wrong or its name is empty. Both break again on regeneration, so the fix belongs in a checklist, not a one-off edit.
How contrast math actually breaks in generated themes
When a model invents a palette, it usually copies a mood board: soft slate text, pastel chips, translucent cards. That looks fine on a designer's monitor and fails the ratios in WCAG 2.2 the moment real content lands. Body text at Level AA needs a 4.5:1 contrast ratio against its background (success criterion 1.4.3). Large text can use 3:1. Generators do not run that math; they pick hex values that "feel secondary."
A second failure mode is the background you cannot see in the Figma export. Text over a hero image, a gradient wash, or a glassmorphism panel needs the rendered background, not the token named bg-surface. Sample the pixel under the glyph, or you will clear a token audit and still ship unreadable copy. Charts and data ink have the same trap; Smashing Magazine's write-up on chart contrast shows how standard thresholds change visual design choices without turning every chart into a rainbow (accessibility standards for chart design).
Non-text contrast is the silent twin
Success criterion 1.4.11 (Non-text Contrast) asks for 3:1 on UI component boundaries, graphical objects needed to understand content, and states such as focus indicators. A faint 1px border that looks "premium" often fails. A focus ring set to outline: 2px solid rgba(15,118,110,.35) fails even when the label text passes 4.5:1. Icon-only controls that rely on a light grey glyph against white are the same class of bug. Treat border, icon, and focus colors as first-class tokens with published ratios, not as decorative afterthoughts. The WCAG 2.2 Quick Reference is the fastest place to re-check which criteria apply at AA.
Tokens beat one-off hex patches
The durable fix is boring: define semantic color tokens (text-primary, text-muted, border-strong, focus-ring) with measured ratios against each allowed background, then forbid the model from inventing hex strings in component files. Put the forbid rule in the prompt and in CI. If regeneration can rewrite color: #94a3b8 into a button label, your "fix" was never a fix. Pair tokens with a CSS variable layer so dark mode and marketing themes inherit the same constraints.
Accessible names are not visible labels
Screen readers announce the accessible name of a control, not the SVG path data. An icon button that shows a trash glyph with no text content, no aria-label, and no associated <label> becomes "button" with no purpose. That is a naming failure, not a contrast failure, which is why color audits stay green while keyboard users stall.
The name computation order is defined by the accessibility tree: content text, labelled-by references, then author-supplied labels such as aria-label. Generators often emit the glyph and a Tailwind class and stop. They also invent duplicate names ("Close" on every dismiss control in a stack of modals) or names that describe the icon metaphor instead of the action ("trash can" instead of "Delete invoice"). Prefer action language that matches the visible affordance.
Where names disappear after regeneration
Regeneration wipes inline aria-label values that were added by hand because the model treats them as optional noise. Survive that by:
- Keeping icon maps in one module that always pairs glyph + name string - Using visible text beside icons when space allows (name comes for free) - Failing CI when an interactive element has an empty accessible name - Rejecting PRs that introduce <div onClick> without a role and name package
WAI's fundamentals overview is still the best short primer on why names, roles, and states must travel together (WAI Accessibility Fundamentals).
When ARIA helps — and when it hides the bug
Native HTML already exposes roles, keyboard behavior, and many states. A real <button> is focusable, activates on Enter and Space, and participates in forms. A <div role="button" tabindex="0"> that a model pasted still needs those key handlers, a pressed or disabled state when relevant, and an accessible name. Skip any of that and assistive tech announces a button that does nothing on Space. The ARIA Authoring Practices Guide (APG) exists for the cases where you truly need a custom widget; it is not a license to restyle a div into every control.
Common ARIA mistakes in vibe-coded markup
- Role on the wrong element: role="button" on an <a href>, or role="link" on a button that submits a form - Decorative ARIA: aria-label that duplicates visible text and confuses some AT combinations - Stale state: aria-expanded="true" left behind after a client render toggles a panel shut - Landmark spam: five role="navigation" regions with no names, so skip lists become noise - Live regions for vanity: aria-live="polite" on every toast so the page chatters
The WAI-ARIA 1.2 recommendation describes the ontology of roles and properties; use it when you must extend beyond HTML, not as a default layer on every node (WAI-ARIA 1.2).
Situation | Prefer | Avoid Primary action | <button type="button"> | <div role="button"> without keys Navigation to a URL | <a href> | Button that only calls router.push with no URL Icon-only control | Visible text or stable aria-label from a map | Bare SVG with title missing Custom combo box | APG pattern + keyboard map | Invented roles copied from a chat answer
Keyboard paths the generator forgets
Focus order, visible focus, and skip links drop out of one-shot layouts. Criterion 2.4.7 requires a visible focus indicator. Criterion 2.4.11 (Focus Not Obscured — Minimum), new in WCAG 2.2, requires that sticky headers, cookie banners, and floating chat widgets do not fully hide the focused control. Tab the generated page top to bottom. Confirm order matches reading order. Confirm no control is stranded behind an overlay. Confirm the focus ring meets 1.4.11 against whatever sits behind it.
A short manual keyboard pass (15 minutes)
- Unplug the mouse. Open the page in a clean profile. - Tab from the browser chrome into the document. Is there a skip link that works? - Reach every primary action without arrow-key folklore the component never implemented. - Open and close every modal with Escape. Does focus return to the invoker? - Trigger the sticky header and the cookie banner. Can you still see the focused control? - Hit every icon button with a screen reader (NVDA or VoiceOver). Does the name match the action?
That pass catches what Lighthouse will not. Automated tools are a first filter; they are not a conformance certificate. For hiring and automated decision tools, U.S. guidance also reminds builders that AI features can create disability discrimination risks if interfaces exclude people who use assistive technology (ADA.gov AI guidance). Product UI is not exempt from that thinking just because a model wrote the JSX.
Make the check survive regeneration
Because regeneration wipes manual patches, lasting control looks like infrastructure:
- Design tokens with measured contrast, checked in a unit test or stylelint plugin - Component primitives (IconButton, TextField) that always emit name + focus styles - CI accessibility gate (axe or equivalent) on pull requests, failing on empty names and contrast - Prompt constraints that ban raw hex and ban div-as-button without an explicit exception flag - Human keyboard pass on each vertical before release, logged as a checklist item, not a vibe
Treat generated markup as untrusted draft output. Review it the same way you review a junior PR that arrived from a weekend hackathon: assume missing names, assume theatrical ARIA, assume pastel text.
Decision criteria: fix in tokens, primitives, or page code?
Use this order when a bug report lands:
- If many pages share the wrong muted text color, fix the token and re-measure ratios. - If one control type lacks names (all icon buttons), fix the primitive and delete local patches. - If a single marketing hero fails over a photo, fix that page's overlay or text treatment. - If ARIA was glued onto the wrong element, replace with a native control before writing more attributes.
Page-level ARIA patches are the least durable. They are also the ones vibe-coding sessions love to emit. Push fixes upward.
What this is NOT
This article is not a certificate of WCAG conformance for your product. It is not legal advice, and it does not claim that running A11yGuard (or any scanner) makes a site "ADA compliant" or "100% accessible." Automated contrast and ARIA checks catch measurable failures; they miss judgment calls about reading order, drag alternatives, and whether a name is understandable. We do not invent customer logos, conversion lifts, or "never miss a contrast bug" guarantees. The honest product role is a fast first pass plus a checklist you still own.
Closing
Contrast math and accessible names fail independently and regenerate together. Lock tokens, prefer native controls, verify ARIA against APG when you must customize, and keep a keyboard pass in the release ritual. Use the references below when a claim about a ratio or a criterion is in dispute — the spec text wins over a chat model's memory.
References
- WCAG 2.2 - WCAG 2.2 Quick Reference - WAI Accessibility Fundamentals - ARIA Authoring Practices Guide (APG) - WAI-ARIA 1.2 - Smashing Magazine — accessibility standards and chart design - ADA.gov — AI and disability discrimination guidance
Related articles
- WCAG 2.2 checklist for AI-generated UI (2026) - Explore the full AI micro-SaaS topic matrix →
Why does my component pass a color audit but still feel broken?
Color audit checks the text ratio. It does not check whether the control has an accessible name, a visible focus state, or a keyboard handler. Those gaps are common in generated markup and are not contrast problems.
Is ARIA enough to make a div behave like a button?
No. A role="button" div still needs tabindex, Enter/Space key handlers, and an accessible name. A native <button> provides all three. Use the native element unless you have a specific reason not to.
How do I stop fixes from disappearing on regeneration?
Lock color and spacing tokens in the design system so the model cannot invent values, and run an automated accessibility gate in CI. Treat generated UI as a draft and review it the same way you review untrusted output.
What is the 3:1 non-text contrast rule?
Criterion 1.4.11 requires UI component boundaries, icons, and focus indicators to reach 3:1 against adjacent colors. It is separate from the 4.5:1 text rule and is frequently missed by generated themes.
Decision-support only. Not legal advice or an accessibility conformity certificate.