tremorlabs/tremor
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Solid tv() architecture and real interaction craft sit right next to invalid HTML that breaks screen readers. The nested anchor-in-button is the one critical flaw dragging down an otherwise disciplined, well-composed component library.
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
Anchor nested inside button breaks keyboard and screen reader navigation
When `onValueChange` is set, `Component` renders as `<button>`. If the row's `item.href` is also set, the inner `<a>{item.name}</a>` renders inside that `<button>`, producing `<button><a>...</a></button>`.
Why it matters
Interactive content nested inside a button is invalid HTML: browsers collapse the nested control's semantics, screen readers announce an ambiguous or broken control, and keyboard users can't reliably reach the link separately from the button's own click action.
Fix
Never nest an interactive element inside another interactive element; when both a row click and a link are needed, render the row as a div with the link as the only focusable control, or move the link out of the clickable wrapper.
<Component
key={item.key ?? item.name}
onClick={() => {
onValueChange?.(item)
}}<Component
key={item.key ?? item.name}
onClick={() => {
if (!item.href) onValueChange?.(item)
}}Default button height lands under the 40px minimum hit area
The shared base class sets `px-3 py-2 text-sm` with a 1px border on most variants. That resolves to roughly 38px tall (16px padding + 20px line-height + 2px border), with no size prop to opt into a taller target.
Why it matters
This is the base size every instance of the component inherits unless overridden, so any primary action rendered at default size on a touch surface sits below the recommended minimum tap target, increasing mis-taps for users with motor impairments.
Fix
Raise vertical padding (or add a min-height) so the default rendered height reaches at least 40px.
"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.5 text-center text-sm font-medium shadow-xs transition-all duration-100 ease-in-out",aria-sort placed on a wrapping div instead of a column header
The outer `<div>` carries `aria-sort={sortOrder}`, but `aria-sort` is only valid on elements with `role="columnheader"` inside a table/grid structure.
Why it matters
Assistive tech either ignores the attribute entirely or reports invalid ARIA, so screen reader users get no actual indication that the list is sorted ascending or descending.
Fix
Only apply aria-sort to a real columnheader element, or drop it and communicate sort order in visible text instead.
aria-sort={sortOrder}
tremor-id="tremor-raw"tremor-id="tremor-raw"Typography
Formatted values lack tabular-nums, so the value column can misalign
The right-aligned value column renders `{valueFormatter(item.value)}` inside a `<p className="truncate whitespace-nowrap text-sm leading-none">` with no `tabular-nums`.
Why it matters
Proportional digit widths mean different values (like "1" vs "188") occupy different widths per character, so the right edge of the value column doesn't line up cleanly across rows, undermining the columnar scan the layout is trying to create.
Fix
Apply tabular-nums to any numeric column that's meant to align vertically.
<p
className={cx(
// base
"truncate whitespace-nowrap text-sm leading-none",<p
className={cx(
// base
"truncate whitespace-nowrap text-sm leading-none tabular-nums",Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeColor
Error badge's ring is noticeably lighter than every sibling variant
In `badgeVariants`, four of five variants use a `/30` ring opacity in light mode: `ring-blue-500/30`, `ring-gray-500/30`, `ring-emerald-600/30`, `ring-yellow-600/30`. The `error` variant alone uses `ring-red-600/20`, a lighter, more washed-out ring.
Why it matters
When success, warning, and error badges sit next to each other (a common status list), the error badge reads as visually weaker than the others, so the most urgent state carries the least visual weight.
Fix
Standardize ring opacity to one value across all variants so visual weight is consistent regardless of status.
error: [
"bg-red-50 text-red-900 ring-red-600/20",
"dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20",
],error: [
"bg-red-50 text-red-900 ring-red-600/30",
"dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20",
],Components
Root Accordion component is mislabeled as AccordionItem in devtools
`Accordion.displayName = "AccordionItem"` is set on the root `Accordion` (aliased from `AccordionPrimitives.Root`), duplicating the name later given to the actual `AccordionItem` component below.
Why it matters
React DevTools and error stack traces will show two different components both named "AccordionItem," making it harder to identify which one is rendering during debugging, and the cost compounds as the component tree grows.
Fix
Set each component's displayName to match its own identity.
Accordion.displayName = "AccordionItem"Accordion.displayName = "Accordion"Spacing
Motion
UX
Craft
Working well
- The trigger's focus ring uses `focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-inset` with `z-10` rather than a default outline. Using `ring-inset` keeps the ring from clipping against the item's bottom border, and `z-10` guarantees it draws above sibling borders, both details that matter once these items stack in a list.
- Using `ring-1 ring-inset` instead of a `border` for every variant is a good call: a ring composites over any surface without shifting the badge's rendered box size, and at low opacity (`/20`-`/30`) it reads as a soft edge rather than a hard outline. This is the correct way to add definition to a small filled element without it looking harsh.
- Every variant defines its light and dark treatment side by side in the same array entry (e.g. `bg-blue-50 text-blue-900 ring-blue-500/30` paired directly with its `dark:` line). This is the right pattern: no variant can ship with a light-mode style and forget the dark-mode counterpart, because they live in the same literal.
- The `isLoading` state disables the button (`disabled={disabled || isLoading}`) and swaps in a spinner with an `sr-only` announcement at the same time. That combination is exactly right: it prevents double-submits (UX-003) and gives screen reader users feedback that sighted users get visually, in a single state change.
Score your own repo with Rams.
Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.