novuhq/novu
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
More findings
Verdict
Custom floating badges look interactive but lead nowhere for keyboard users, while the underlying Tiptap plumbing is handled with real care. The biggest risk is that unusable controls like the repeat indicator and code toggle sit right next to well-built native pieces, so the gap reads as inconsistency, not incapacity.
Files Rams reviewed
packages/novu/src/commands/init/templates/app-react-email/ts/app/components/NotificationToast/Notifications.module.css
playground/nextjs/src/app/agents-mcp/components/flow-diagram.tsx
apps/dashboard/src/components/maily/views/for-view.tsx
apps/dashboard/src/components/maily/views/html-view.tsx
apps/dashboard/src/components/maily/views/maily-variables-list-view.tsx
apps/dashboard/src/components/maily/views/variable-view.tsx
apps/dashboard/src/pages/access-denied-page.tsx
apps/dashboard/src/pages/agent-slack-setup-page.tsx
apps/dashboard/src/pages/agent-telegram-mobile-setup-page.tsx
apps/dashboard/src/pages/agent-whatsapp-signup-page.tsx
apps/dashboard/src/pages/edit-layout.tsx
apps/dashboard/src/pages/inbox-embed-page.tsx
apps/dashboard/src/pages/inbox-embed-success-page.tsx
apps/dashboard/src/pages/inbox-usecase-page.tsx
apps/dashboard/src/pages/integration-store-telegram-mobile-setup-page.tsx
apps/dashboard/src/pages/integrations-list-page.tsx
apps/dashboard/src/pages/server-error-page.tsx
apps/dashboard/src/pages/vercel-integration-page.tsx
apps/dashboard/src/pages/webhooks-page.tsx
apps/dashboard/src/pages/welcome-page.tsx
packages/novu/src/commands/init/templates/app-agent-ai-sdk/ts/app/page.tsx
packages/novu/src/commands/init/templates/app-agent-langchain/ts/app/page.tsx
packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx
playground/nextjs/src/app/agent-toolkit/page.tsx
playground/nextjs/src/app/novu-agent/page.tsx
apps/dashboard/src/components/agents/agent-integration-guides/agent-integration-guide-layout.tsx
apps/dashboard/src/components/agents/public-token-page.tsx
apps/dashboard/src/components/dashboard-layout.tsx
apps/dashboard/src/components/translations/translation-onboarding-page.tsx
apps/dashboard/src/components/workflow-editor/steps/conditions/edit-step-conditions-layout.tsx
Accessibility
apps/dashboard/src/components/maily/views/for-view.tsx:18
Repeat block indicator is unreachable and unusable by keyboard
In `ForView`, the `div` with `role="button"` and `data-repeat-indicator` (lines 18-25) fires `editor.commands.setNodeSelection` on `onClick`, but it has no `tabIndex`, no `onKeyDown`, and no native `<button>` semantics. A screen reader or keyboard user can tab through the editor but will never land on this control.
Why it matters
Keyboard-only and screen reader users cannot select the repeat block at all, which locks them out of a core editing action available to mouse users.
Fix
Replace the div with a native button element, or add tabIndex=0 and an onKeyDown handler that fires on Enter and Space.
<div
role="button"
data-repeat-indicator=""
contentEditable={false}
onClick={() => {
editor.commands.setNodeSelection(getPos());
}}
className="border-soft-100 absolute right-[-2px] top-[-3px] flex cursor-grab items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px]"
><button
type="button"
data-repeat-indicator=""
contentEditable={false}
onClick={() => {
editor.commands.setNodeSelection(getPos());
}}
className="border-soft-100 absolute right-[-2px] top-[-3px] flex cursor-grab items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px]"
>apps/dashboard/src/components/maily/views/html-view.tsx:94
Code/preview toggle for HTML block is a div with no keyboard path
The outer wrapper `<div className="group relative cursor-pointer" onClick={onClick}>` (line 94) is the sole way to toggle between code and preview mode, but it carries no `role`, `tabIndex`, or key handler.
Why it matters
Keyboard-only users cannot switch the HTML block into preview mode at all, cutting off a primary editing interaction from anyone not using a mouse.
Fix
Give the toggle native button semantics, or add role="button", tabIndex={0}, and an onKeyDown handler for Enter and Space.
<div className="group relative cursor-pointer" onClick={onClick}><div
role="button"
tabIndex={0}
className="group relative cursor-pointer"
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick?.();
}
}}
>Color
apps/dashboard/src/components/maily/views/html-view.tsx:98
Preview container border hardcodes a hex value instead of a token
The inner preview div's hover border uses `group-hover:border-[#E4E4E7]` (line 98) as a raw hex value, while the same file elsewhere uses the `border-soft-100` token for identical gray borders (lines 25, 110).
Why it matters
Two different sources for the same gray means a future palette or dark-mode change updates one border and misses the other, so the preview hover state silently drifts from the rest of the editor's borders.
Fix
Replace the hardcoded hex with the existing border-soft-100 token already used for the same visual role elsewhere in this file.
'-mx-2 min-h-[42px] rounded-md border px-2',
'border-transparent group-hover:border-[#E4E4E7]',
'flex flex-col justify-center''-mx-2 min-h-[42px] rounded-md border px-2',
'border-transparent group-hover:border-soft-100',
'flex flex-col justify-center'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 freeUX
apps/dashboard/src/components/maily/views/html-view.tsx:110
'html' badge shows a grab cursor for a drag that never happens
The floating badge with the `RiCodeBlock` icon and "html" label (line 110) is styled with `cursor-grab`, signaling a draggable handle. Nothing in this component wires up drag behavior on that element; it only appears on `group-hover` as a static indicator.
Why it matters
A grab cursor tells users they can drag the block from this handle. When dragging does nothing, the mismatch between affordance and behavior costs the user a failed attempt and erodes trust in every other drag handle in the editor.
Fix
Match the cursor style to the actual interaction: use cursor-default or cursor-pointer if the badge is only a status indicator, and reserve cursor-grab for elements with real drag handlers.
<div className="border-soft-100 absolute right-[-10px] top-[-3px] hidden cursor-grab items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px] group-hover:flex"><div className="border-soft-100 absolute right-[-10px] top-[-3px] hidden cursor-default items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px] group-hover:flex">Craft
apps/dashboard/src/components/maily/views/for-view.tsx:25
Repeat badge positioning uses four one-off pixel values
The repeat indicator's wrapper div (line 24-25) uses `right-[-2px]`, `top-[-3px]`, `gap-[2px]`, and `py-[2px]`, all arbitrary Tailwind values instead of scale tokens like `-right-0.5`, `-top-1`, `gap-0.5`, `py-0.5`.
Why it matters
Every arbitrary value is a number nobody chose deliberately and nobody will remember to update; small positioning drifts accumulate across the editor's badges and nothing stays visually aligned as the design system evolves.
Fix
Use Tailwind's spacing scale tokens instead of arbitrary pixel values so the badge stays consistent with other floating indicators.
className="border-soft-100 absolute right-[-2px] top-[-3px] flex cursor-grab items-center justify-center gap-[2px] rounded border bg-white px-1 py-[2px]"className="border-soft-100 absolute -right-0.5 -top-1 flex cursor-grab items-center justify-center gap-0.5 rounded border bg-white px-1 py-0.5"Typography
Spacing
Components
Motion
Working well
- The HTML view keeps NodeViewContent mounted during preview mode with a comment explaining it's required for ProseMirror's contentDOM to stay attached. That's a deliberate, documented tradeoff rather than an accidental leftover, and it's the right call given the constraint.
- The repeat block's NodeViewWrapper uses draggable="true" with data-drag-handle, leveraging Tiptap's native drag system instead of building a custom drag implementation from scratch, which keeps drag behavior consistent with the rest of the editor's node types.
Scored August 1, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored July 29, 2026: 59/100. This rescore on v0.0.3: 59/100.
This page is an automated design review of novuhq/novu’s UI code: 30 files read against 291 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.