novuhq/novu
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 29, 2026
Verdict
This file mixes a genuine accessibility gap with a handful of small styling debts. The keyboard-inaccessible preview toggle is the one that actually blocks a class of users and should get fixed first; the hardcoded hex and scattered arbitrary pixel values are lower-stakes drift that make the block's visual language harder to keep consistent over time. The shadow DOM isolation and click-outside handling show the author thought carefully about the harder integration problems, which makes the smaller polish gaps worth closing too.
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
Spacing
apps/dashboard/src/components/maily/views/html-view.tsx:110
Html-tag badge stacks five arbitrary offsets in one class list
The floating "html" badge div uses `right-[-10px] top-[-3px] ... gap-[2px] ... py-[2px]`, four arbitrary pixel values plus the earlier arbitrary min-height, all hand-tuned rather than pulled from a spacing scale.
Why it matters
Every one-off pixel value here is a value nobody else can reuse, so the next badge or overlay in this codebase gets its own hand-tuned offsets instead of a shared pattern, and small inconsistencies in positioning accumulate across the editor.
Fix
Replace the arbitrary negative offsets and gaps with the nearest scale steps (e.g. -right-2.5, -top-1, gap-0.5, py-0.5) so the badge positioning stays on the same scale as the rest of the UI.
<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-2.5 -top-1 hidden cursor-grab items-center justify-center gap-0.5 rounded border bg-white px-1 py-0.5 group-hover:flex">apps/dashboard/src/components/maily/views/html-view.tsx:97
Preview wrapper's min-height is an arbitrary pixel value
The content wrapper div sets `min-h-[42px]`, a one-off pixel value with no relation to the rest of the spacing/sizing scale used in this component.
Why it matters
Arbitrary pixel heights like this can't be reused or reasoned about alongside other block heights in the editor, so future blocks either copy the magic number or introduce yet another one-off, and the editor's vertical rhythm drifts block by block.
Fix
Replace the arbitrary pixel height with a scale token (e.g. min-h-10/min-h-11) or derive it from existing spacing utilities used elsewhere in the editor.
'-mx-2 min-h-[42px] rounded-md border px-2','-mx-2 min-h-10 rounded-md border px-2',Accessibility
apps/dashboard/src/components/maily/views/html-view.tsx:108
Clickable preview div has no keyboard path into edit mode
The PreviewView wrapper `<div className="group relative cursor-pointer" onClick={onClick}>` is the only way to switch this HTML block from preview into code-editing mode, but it carries no role, no tabIndex, and no onKeyDown handler. A mouse click works; a keyboard user tabbing through the editor has no way to focus this div or trigger onClick from it.
Why it matters
Keyboard-only users and screen reader users cannot enter edit mode on this block at all, which locks them out of a core editing action in the email editor.
Fix
Give the clickable div button semantics: add role="button", tabIndex={0}, and an onKeyDown handler that fires onClick on Enter/Space, or replace the div with a real <button>.
<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?.();
}
}}
>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
apps/dashboard/src/components/maily/views/html-view.tsx:98
Hover border uses raw hex instead of the border token
The content wrapper's hover state sets `group-hover:border-[#E4E4E7]`, a raw hex value, while the sibling html-tag badge just below it uses `border-soft-100` for the same kind of border. Two different sourcing methods for what should be one border color.
Why it matters
When the border token changes (theme update, dark mode), this hardcoded hex won't move with it, so the hover border silently drifts out of sync with the rest of the editor chrome.
Fix
Replace the hardcoded hex with the existing border token used elsewhere in this same component.
'border-transparent group-hover:border-[#E4E4E7]','border-transparent group-hover:border-soft-100',Typography
Components
Motion
UX
Craft
Working well
- Using shadow DOM (node.attachShadow) to isolate injected email HTML styles from the editor's own CSS is a smart, deliberate containment technique that prevents email markup from leaking styles into the surrounding editor chrome.
- The click-outside handler explicitly excludes tippy-box and radix popper wrappers before deciding a click is 'outside,' which shows real care about not closing the code view out from under an open bubble menu.
- The preview node correctly sets contentEditable={false} on the shadow-DOM host, so the injected HTML never gets swallowed into the editable node content and corrupted by the rich-text editor.
Scored July 29, 2026 with Rams Engine v0.0.3 · Engine changelog
This page is an automated design review of novuhq/novu’s UI code: 30 files read against 258 versioned rules covering accessibility, color, typography, spacing, components, UX, motion, and craft. The score is out of 100; any confirmed critical issue caps it at 59.
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.