heroui-inc/heroui
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
Elevated
Design risk in this codebase.
More findings
Verdict
Promotional Pro surfaces get the gradient treatment while losing sight of who can see or reach them. The biggest risk is a close control and a link that are both literally invisible yet still interactive, a pattern that undermines trust in the whole design system.
Files Rams reviewed
apps/docs/src/app/docs/native-showcase/components/[[...slug]]/page.tsx
apps/docs/src/app/[lang]/(home)/components/demo-showcase.tsx
apps/docs/src/app/[lang]/(home)/components/pro-badge.tsx
apps/docs/src/app/[lang]/(home)/components/pro-banner.tsx
apps/docs/src/app/[lang]/(home)/components/pro-title.tsx
apps/docs/src/app/[lang]/(home)/components/release-badges.tsx
apps/docs/src/app/[lang]/themes/components/accent-color-selector.tsx
apps/docs/src/app/[lang]/themes/components/bottom-sheet.tsx
apps/docs/src/app/[lang]/themes/components/builder-header.tsx
apps/docs/src/app/[lang]/themes/components/chroma-slider.tsx
apps/docs/src/app/[lang]/themes/components/custom-fonts.tsx
apps/docs/src/app/[lang]/themes/components/font-family-popover.tsx
apps/docs/src/app/[lang]/themes/components/lockable-label.tsx
apps/docs/src/app/[lang]/themes/components/mobile-footer.tsx
apps/docs/src/app/[lang]/themes/components/onboarding.tsx
apps/docs/src/app/[lang]/themes/components/preview-container.tsx
apps/docs/src/app/[lang]/themes/components/radius-popover.tsx
apps/docs/src/app/[lang]/themes/components/reset-button.tsx
apps/docs/src/app/[lang]/themes/components/shuffle-button.tsx
apps/docs/src/app/[lang]/themes/components/suggested-fonts.tsx
apps/docs/src/app/[lang]/themes/components/switch-mode.tsx
apps/docs/src/app/[lang]/themes/components/theme-builder-content.tsx
apps/docs/src/app/[lang]/themes/components/theme-code-panel.tsx
apps/docs/src/app/[lang]/themes/components/theme-input.tsx
apps/docs/src/app/[lang]/themes/components/theme-popover.tsx
apps/docs/src/app/[lang]/themes/components/themes-list.tsx
apps/docs/src/app/[lang]/(home)/layout.tsx
apps/docs/src/app/[lang]/(home)/page.tsx
apps/docs/src/app/[lang]/(home)/showcase/layout.tsx
apps/docs/src/app/[lang]/blog/[slug]/page.tsx
Accessibility
apps/docs/src/app/[lang]/(home)/components/pro-banner.tsx:254
Pro banner close control is invisible against its own background
The dismiss control on the pro banner (CloseButton, absolute end-2 top-2) uses text-white/50 over the light lavender-to-blue gradient (#E9E9FF to #CCE5F1) that fills the banner header. White at 50% opacity over a near-white background computes to roughly 1.2:1 contrast, so the icon is effectively invisible against its own fill.
Why it matters
Users who want to dismiss the banner can't find the control by sight, and the ones who do find it by trial-and-error location have no visual confirmation it's interactive. A permanently fixed, unreadable close button on a floating banner blocks a core exit path.
Fix
Set close-icon contrast to at least 4.5:1 against the header gradient using a dark neutral token instead of a translucent white.
className="absolute end-2 top-2 bg-transparent text-white/50 hover:bg-white/10 hover:text-white"className="absolute end-2 top-2 bg-transparent text-foreground/70 hover:bg-black/10 hover:text-foreground"apps/docs/src/app/[lang]/(home)/components/demo-showcase.tsx:236
Invisible 'Pro template' link stays reachable by keyboard on the components tab
The "Pro template" anchor in demo-showcase.tsx toggles to opacity-0 pointer-events-none when selectedTab is 'components', but it stays in the DOM and in tab order. A keyboard user tabbing through the toolbar lands on a link with no visible focus target.
Why it matters
Keyboard users hit a focus stop with nothing to see, breaking the expected correspondence between visible UI and tab order and making the toolbar feel broken.
Fix
Remove hidden interactive elements from the tab order with tabIndex={-1} and aria-hidden whenever they're visually suppressed.
<a
className={`text-[13px] font-medium text-muted transition-opacity duration-300 hover:text-foreground ${selectedTab !== "components" ? "opacity-100" : "pointer-events-none opacity-0"}`}
rel="noopener noreferrer"
target="_blank"
href={getProUrl({ campaign: "demo-showcase", content: `template-${selectedTab}`, medium: "homepage" })}
>
{demo.proTemplate}
</a><a
className={`text-[13px] font-medium text-muted transition-opacity duration-300 hover:text-foreground ${selectedTab !== "components" ? "opacity-100" : "pointer-events-none opacity-0"}`}
rel="noopener noreferrer"
target="_blank"
tabIndex={selectedTab !== "components" ? 0 : -1}
aria-hidden={selectedTab === "components"}
href={getProUrl({ campaign: "demo-showcase", content: `template-${selectedTab}`, medium: "homepage" })}
>
{demo.proTemplate}
</a>apps/docs/src/app/[lang]/(home)/components/demo-showcase.tsx:253
Color swatches in the theme picker have no accessible names
The ColorSwatchPicker.Item elements in demo-showcase.tsx render only a Swatch and an Indicator for each hex value in colors, with no aria-label or visible text tied to the color name.
Why it matters
A screen reader user navigating the picker hears identical unlabeled controls for every option and can't tell 'pink' from 'red' from 'orange', making the accent-color choice unusable without sight.
Fix
Give every non-text control a computed accessible name so it doesn't rely on color perception alone.
{colors.map((color) => (
<ColorSwatchPicker.Item key={color} color={color}>
<ColorSwatchPicker.Swatch />
<ColorSwatchPicker.Indicator />
</ColorSwatchPicker.Item>
))}{colors.map((color) => (
<ColorSwatchPicker.Item key={color} color={color} aria-label={color}>
<ColorSwatchPicker.Swatch />
<ColorSwatchPicker.Indicator />
</ColorSwatchPicker.Item>
))}Color
apps/docs/src/app/[lang]/(home)/components/pro-banner.tsx:236
Countdown label text fails contrast against the banner's gradient header
The "Ends in" countdown label in pro-banner.tsx uses text-[#4E75A5] at text-xs size over a gradient header that runs from #E9E9FF to #CCE5F1. That combination computes to roughly 4:1 contrast, which fails WCAG AA's 4.5:1 threshold for small text.
Why it matters
Low-vision users reading the discount countdown at 12px see faint blue-gray text against a near-white background, making time-sensitive promotional copy hard to read at the moment it matters most.
Fix
Darken small text to reach at least 4.5:1 contrast against its lightest background stop.
<span className="relative text-xs font-medium text-[#4E75A5]"><span className="relative text-xs font-medium text-[#33507A]">Get this score on every PR.
Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.
Install Rams freeComponents
apps/docs/src/app/[lang]/(home)/components/pro-banner.tsx:322
Pro CTA overrides its own primary variant with hardcoded black/white
The "Explore Pro" / "Get Pro deal" link passes variant: "primary" into buttonVariants but then overrides it with className: "bg-black text-white dark:bg-white dark:text-black" in pro-banner.tsx, bypassing whatever fill and text pairing the primary variant defines.
Why it matters
When this button's primary variant changes anywhere else in the design system, this instance silently stays black/white, so future rebrands or accent updates miss this CTA and the button drifts out of sync with every other primary action.
Fix
Let the primary variant supply its own background and text color instead of overriding it locally.
className={buttonVariants({
className: "bg-black text-white dark:bg-white dark:text-black",
variant: "primary",
})}className={buttonVariants({
variant: "primary",
})}UX
apps/docs/src/app/[lang]/(home)/components/release-badges.tsx:107
Touch users have no way to pause the badge carousel before it rotates away
The badge carousel pauses only on onMouseEnter and onFocusCapture in release-badges.tsx. Touch devices have neither persistent hover nor a tap-to-focus equivalent, so a mobile user reading a badge has no way to stop the interval before it advances.
Why it matters
Mobile visitors, a large share of homepage traffic, lose the ability to read a badge on their own schedule, and the content the carousel exists to show becomes read-only-if-you're-fast.
Fix
Give touch users an explicit pause affordance instead of relying solely on hover and focus events.
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
onTouchStart={() => setPaused((p) => !p)}Typography
Spacing
Motion
Craft
Working well
- This is a static SVG icon component with hardcoded gradient/fill hex values, which is standard and correct practice for brand icon assets: icons like this are logo-like and not part of the themeable UI token system.
- The four gradient stops all reuse the exact same three-color ramp (#617BF8 to #6AC6F7 to #90CAF6) across every path, which keeps the mark visually coherent instead of drifting hue per letterform.
- The comments explaining why dynamicParams is true and why the native app intercepts this route before the browser loads it are genuinely useful context, not narration filler restating the code.
- The theme builder link uses Tooltip with delay={0} paired with an isIconOnly ghost button and Palette icon, giving an icon-only control a discoverable label on hover.
Scored August 1, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored July 9, 2026: 59/100. This rescore on v0.0.3: 59/100.
This page is an automated design review of heroui-inc/heroui’s UI code: 30 files read against 309 versioned rules covering accessibility, color, typography, spacing, components, UX, motion, and craft. The score is out of 100; confirmed criticals cap it — one at 59, two at 49, three or more at 39.
More design scores
Score your own repo.
Free on public repos, no account. The same engine that scored this page reads your UI code and mints a score page like this one.
Public repos only. The full engine reviews the UI code and mints a public score page — we email you the link too. Already-scored repos open instantly.