tremorlabs/tremor
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
Good instincts, unverified numbers: this library keeps reaching for accessible patterns without ever measuring them against a real threshold. A spinner that animates forever with no reduced-motion guard is the clearest sign that motion and a11y checks were skipped, not just missed.
Files Rams reviewed
src/components/Accordion/Accordion.tsx
src/components/Badge/Badge.tsx
src/components/BarList/BarList.tsx
src/components/Button/Button.tsx
src/components/Calendar/Calendar.tsx
src/components/Callout/Callout.tsx
src/components/CategoryBar/CategoryBar.tsx
src/components/Checkbox/Checkbox.tsx
src/components/Dialog/Dialog.tsx
src/components/Divider/Divider.tsx
src/components/DonutChart/DonutChart.tsx
src/components/Drawer/Drawer.tsx
src/components/DropdownMenu/DropdownMenu.tsx
src/components/Input/Input.tsx
src/components/Popover/Popover.tsx
src/components/ProgressBar/ProgressBar.tsx
src/components/ProgressCircle/ProgressCircle.tsx
src/components/RadioCardGroup/RadioCardGroup.tsx
src/components/RadioGroup/RadioGroup.tsx
src/components/Select/Select.tsx
src/components/SelectNative/SelectNative.tsx
src/components/Slider/Slider.tsx
src/components/SparkChart/SparkChart.tsx
src/components/Switch/Switch.tsx
src/components/TabNavigation/TabNavigation.tsx
src/components/Table/Table.tsx
src/components/Tabs/Tabs.tsx
src/components/Textarea/Textarea.tsx
src/components/Toast/Toast.tsx
src/components/Toggle/Toggle.tsx
Accessibility
src/components/BarList/BarList.tsx:59
aria-sort applied to a plain div instead of a table header
The outer BarList container is a <div> carrying "aria-sort={sortOrder}". aria-sort is only a valid ARIA attribute on an element with role="columnheader" (typically a <th>).
Why it matters
Applied to a non-header div, the attribute is invalid ARIA that screen readers ignore or that accessibility linters flag, so the sort state is never actually communicated to assistive tech users.
Fix
Drop aria-sort from the div and communicate sort state through visible UI or a live region instead.
<div
ref={forwardedRef}
className={cx("flex justify-between space-x-6", className)}
aria-sort={sortOrder}
tremor-id="tremor-raw"
{...props}
><div
ref={forwardedRef}
className={cx("flex justify-between space-x-6", className)}
tremor-id="tremor-raw"
{...props}
>src/components/Calendar/Calendar.tsx:49
Calendar navigation buttons render below the 44px touch target floor
NavigationButton (used for previous/next month and year) sizes at "size-8" (32px) and shrinks further to "sm:size-[30px]" on larger breakpoints.
Why it matters
A 30-32px hit target on a date picker falls under the 44px minimum recommended for touch targets, making the prev/next controls hard to hit accurately on mobile.
Fix
Size icon-only navigation controls to at least 44px on touch breakpoints.
"flex size-8 shrink-0 select-none items-center justify-center rounded-sm border p-1 outline-hidden transition sm:size-[30px]","flex size-11 shrink-0 select-none items-center justify-center rounded-sm border p-1 outline-hidden transition sm:size-11",src/components/Callout/Callout.tsx:79
Overflowing callout content has no keyboard path to scroll it
The content wrapper at line 79 sets "overflow-y-auto" on {children} but has no tabIndex, so it never enters the tab order.
Why it matters
When Callout content overflows its container, mouse users can scroll it with a wheel but keyboard-only users have no way to focus the region and scroll to the rest of the content.
Fix
Give scrollable overflow regions tabIndex=0 so keyboard users can focus and scroll them.
<div className={cx("overflow-y-auto", children ? "mt-2" : "")}>
{children}
</div><div
tabIndex={0}
className={cx("overflow-y-auto", children ? "mt-2" : "")}
>
{children}
</div>Motion
src/components/Button/Button.tsx:130
Loading spinner spins forever with no reduced-motion guard
The isLoading state renders RiLoader2Fill with "animate-spin" and no prefers-reduced-motion check anywhere in the component.
Why it matters
Vestibular-sensitive users get continuous, unstoppable motion on every loading button in the system with no way to opt out.
Fix
Wrap the spin animation in a motion-safe: variant so it respects the user's reduced-motion preference.
<RiLoader2Fill
className="size-4 shrink-0 animate-spin"
aria-hidden="true"
/><RiLoader2Fill
className="size-4 shrink-0 motion-safe:animate-spin"
aria-hidden="true"
/>src/components/Button/Button.tsx:14
transition-all on the button base forces the browser to watch every property
The shared base class list applies "transition-all duration-100 ease-in-out" to every Button variant instead of naming the properties that actually change (background, border, box-shadow, color).
Why it matters
Watching every animatable property costs more per frame and can trigger unintended transitions the next time an unrelated CSS property changes on the element, which is wasted work multiplied across every button instance.
Fix
Scope the transition to the properties that actually change on state.
"relative inline-flex items-center justify-center whitespace-nowrap rounded-md border px-3 py-2 text-center text-sm font-medium shadow-xs transition-all duration-100 ease-in-out","relative inline-flex items-center justify-center whitespace-nowrap rounded-md border px-3 py-2 text-center text-sm font-medium shadow-xs transition-[color,background-color,border-color,box-shadow] duration-100 ease-in-out",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 freeColor
src/components/Button/Button.tsx:24
Primary button text fails contrast against its own blue fill
The primary Button variant pairs "text-white" with "bg-blue-500" for every primary CTA in the system. White (#fff) on blue-500 (#3b82f6) computes to 3.68:1.
Why it matters
3.68:1 fails WCAG AA's 4.5:1 threshold for normal-size text, so the label on every primary action across the product reads at reduced contrast for low-vision users.
Fix
Darken the primary fill to blue-600 or deepen it further so white text clears 4.5:1.
"text-white dark:text-white",
"bg-blue-500 dark:bg-blue-500","text-white dark:text-white",
"bg-blue-600 dark:bg-blue-600",Typography
Spacing
Components
UX
Craft
Working well
- NavigationButton always ships an explicit aria-label ('Go to previous year', 'Go to next month', etc.) even though it's icon-only, which is exactly the right pattern for icon-only controls: the label carries the meaning that the icon alone can't.
- Button's isLoading state hides children behind an sr-only announcement ('Loading' or a custom loadingText) while keeping the spinner icon aria-hidden, giving screen readers a single clean announcement instead of raw icon noise.
- BarList's row Component switches between button and div based on whether onValueChange is passed, so keyboard focus and interactive semantics only show up when the row is actually clickable.
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 tremorlabs/tremor’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.