Rams MCP · The full engine, now in your coding agent
tremorlabs on GitHub

tremorlabs/tremor

Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.

30 files reviewed·July 9, 2026

Elevated

Design risk in this codebase.

6issues
Critical & Serious

Top fix

Remove anchor nested inside button to fix keyboard navigation

See the fix

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

93/100

Accessibility

1 critical2 serious
Accessibility·src/components/BarList/BarList.tsx:75Critical

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)
  }}
Accessibility·src/components/Button/Button.tsx:14Serious

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",
Accessibility·src/components/BarList/BarList.tsx:60Serious

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"
98/100

Typography

1 serious
Typography·src/components/BarList/BarList.tsx:155Serious

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 free
98/100

Color

1 serious
Color·src/components/Badge/Badge.tsx:25Serious

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",
],
98/100

Components

1 serious
Components·src/components/Accordion/Accordion.tsx:10Serious

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

No issues found

Motion

No issues found

UX

No issues found

Craft

No issues found

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.