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

langgenius/dify

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

30 files reviewed·July 9, 2026

Elevated

Accessibility needs attention.

6issues
Critical & Serious

Top fix

Replace the logout span with a real button element

See the fix

Verdict

Solid state machines and thoughtful interaction logic keep getting undermined by non-semantic markup: spans standing in for buttons, divs standing in for headings, focus rings stripped with nothing replacing them. The biggest risk is keyboard and screen reader users hitting real dead ends on critical paths like logout and skip-nav.

Files Rams reviewed

web/app/(shareLayout)/components/authenticated-layout.tsx

web/app/components/header/account-setting/model-provider-page/model-selector/popup-layout.tsx

web/app/components/integrations/page.tsx

web/app/components/integrations/plugin-category-page.tsx

web/app/components/main-nav/layout.tsx

web/app/components/access-rules-editor/index.tsx

web/app/components/app-sidebar/app-info/index.tsx

web/app/components/app-sidebar/dataset-info/index.tsx

web/app/components/app-sidebar/nav-link/index.tsx

web/app/components/app/access-config/index.tsx

web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx

web/app/components/app/annotation/add-annotation-modal/index.tsx

web/app/components/app/annotation/batch-add-annotation-modal/index.tsx

web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx

web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx

web/app/components/app/annotation/edit-annotation-modal/index.tsx

web/app/components/app/annotation/header-opts/index.tsx

web/app/components/app/annotation/index.tsx

web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx

web/app/components/app/annotation/view-annotation-modal/index.tsx

web/app/components/app/app-access-control/index.tsx

web/app/components/app/configuration/base/feature-panel/index.tsx

web/app/components/app/configuration/base/operation-btn/index.tsx

web/app/components/app/configuration/base/warning-mask/index.tsx

web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx

web/app/components/app/configuration/config-prompt/index.tsx

web/app/components/app/configuration/config-var/config-modal/index.tsx

web/app/components/app/configuration/config-var/config-select/index.tsx

web/app/components/app/configuration/config-var/index.tsx

web/app/components/app/configuration/config-vision/index.tsx

89/100

Accessibility

1 critical4 serious
Accessibility·web/app/(shareLayout)/components/authenticated-layout.tsx:84Critical

Logout control is a clickable span, invisible to keyboard and screen readers

The only way out of the 403 'no permission' screen is `<span className="cursor-pointer ..." onClick={backToHome}>{t('userProfile.logout')}</span>`. It has no button semantics, no tabIndex, no keyboard handler, and no focus ring.

Why it matters

A keyboard user or screen reader user who lands on the 403 state has zero way to log out and retry, since the element is unreachable by Tab and unannounced by assistive tech. This is the single exit path off a dead-end screen.

Fix

Use a native `<button>` for any element with an onClick handler so it gets keyboard focus, Enter/Space activation, and a focus ring for free.

<span className="cursor-pointer system-sm-regular text-text-tertiary" onClick={backToHome}>{t('userProfile.logout', { ns: 'common' })}</span>
<button type="button" className="system-sm-regular text-text-tertiary hover:underline" onClick={backToHome}>{t('userProfile.logout', { ns: 'common' })}</button>
Accessibility·web/app/components/integrations/plugin-category-page.tsx:87Serious

Drop-zone activation is announced with color and border alone

When `dragging` is true, the only feedback is a dashed-border overlay div with no text content, `role`, or `aria-live` region anywhere in the component.

Why it matters

Screen-reader users dragging a file over the panel get no announcement that a drop target is now active, so they can't confirm the drop will register before releasing the file.

Fix

Pair visual drag-state changes with an `aria-live` region or `role="status"` announcing the state change.

{dragging && (
  <div
    className="absolute inset-0 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent
      bg-[rgba(21,90,239,0.14)] p-2"
  />
)}
{dragging && (
  <div
    role="status"
    className="absolute inset-0 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent
      bg-components-dropzone-bg-accent p-2"
  >
    <span className="sr-only">Drop file to install plugin</span>
  </div>
)}
Accessibility·web/app/components/integrations/page.tsx:236Serious

Page title 'Integrations' renders as a div, not a heading

The sidebar title uses `<div className="...title-2xl-semi-bold text-text-primary">{t('settings.integrations')}</div>` instead of a semantic heading element.

Why it matters

Screen reader users navigate by heading level to jump straight to page sections. A div with heading-looking text is invisible to that navigation model, so assistive tech users can't confirm they've landed on the Integrations page or skip to it directly.

Fix

Use a semantic heading element for the primary page title, styled with the existing typography class.

<div className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
  {t('settings.integrations', { ns: 'common' })}
</div>
<h1 className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
  {t('settings.integrations', { ns: 'common' })}
</h1>
Accessibility·web/app/components/main-nav/layout.tsx:66Serious

Skip-nav target strips its own focus ring with no replacement

The `<main>` element is the destination for the `SkipNav` link (`id={MAIN_CONTENT_ID}`, `tabIndex={-1}`) but its className chains `outline-hidden focus:outline-hidden focus-visible:outline-hidden`, removing the browser's default focus indicator in every focus state.

Why it matters

A keyboard user who activates "Skip to main content" gets no visual confirmation that focus actually moved. The entire point of a skip link is to jump focus past the nav, and here that jump is invisible, so sighted keyboard users can't tell if the link worked or if they need to keep tabbing.

Fix

Never remove a focus outline without a visible replacement; give the skip-nav landing target its own focus-visible ring.

className="flex min-h-0 min-w-0 grow flex-col overflow-hidden outline-hidden focus:outline-hidden focus-visible:outline-hidden"
className="flex min-h-0 min-w-0 grow flex-col overflow-hidden outline-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-accent"
Accessibility·web/app/components/header/account-setting/model-provider-page/model-selector/popup-layout.tsx:64Serious

Search clear button hit area is 14px, far below tap minimum

The clear button in `ModelSelectorSearchHeader` is sized `size-3.5` (14px) with the icon filling the whole button: - `<button ... className="ml-1.5 flex size-3.5 shrink-0 ...">` There is no padding or invisible hit-area extension around it.

Why it matters

A 14px target sits well under the WCAG 2.2 AA floor of 24px, so users on touch devices or with motor impairments repeatedly miss the tap and can't clear their search.

Fix

Extend the button's hit area to at least 24px while keeping the visible icon small, using a pseudo-element or padding.

<button
  type="button"
  aria-label={t('operation.clear', { ns: 'common' }) || 'Clear'}
  className="ml-1.5 flex size-3.5 shrink-0 cursor-pointer items-center justify-center rounded-none text-text-quaternary outline-hidden hover:bg-transparent hover:text-text-quaternary focus-visible:bg-transparent focus-visible:ring-1 focus-visible:ring-components-input-border-active"
  onClick={() => onInputValueChange('')}
  onPointerDown={event => event.preventDefault()}
>
<button
  type="button"
  aria-label={t('operation.clear', { ns: 'common' }) || 'Clear'}
  className="relative ml-1.5 flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-none text-text-quaternary outline-hidden after:absolute after:inset-0 hover:bg-transparent hover:text-text-quaternary focus-visible:bg-transparent focus-visible:ring-1 focus-visible:ring-components-input-border-active"
  onClick={() => onInputValueChange('')}
  onPointerDown={event => event.preventDefault()}
>
98/100

Spacing

1 serious
Spacing·web/app/components/access-rules-editor/index.tsx:92Serious

Table header and row columns don't share a horizontal inset

The header row uses `px-10 py-4` on the grid container, but the row list wrapper below it uses `px-4`. Both use the same `ACCESS_RULE_TABLE_GRID` column definition.

Why it matters

A 24px inset mismatch between header and body means the "Member", "Permission", and "Actions" column labels no longer line up over their data, breaking scan alignment across the table.

Fix

Give the header and the row container the same horizontal padding so grid columns stay aligned.

<div className={cn('grid items-center gap-4 border-b border-divider-deep px-10 py-4 system-sm-semibold text-text-tertiary', ACCESS_RULE_TABLE_GRID)}>
<div className={cn('grid items-center gap-4 border-b border-divider-deep px-4 py-4 system-sm-semibold text-text-tertiary', ACCESS_RULE_TABLE_GRID)}>

Typography

No issues found

Color

No issues found

Components

No issues found

Motion

No issues found

UX

No issues found

Craft

No issues found

Working well

  • The `DescriptionWithLearnMore` component truncates its text with `truncate` but only sets `title={children}` when children is a string, so the full description is still recoverable on hover. This is exactly the right pairing: truncation without a recovery path (a bare `truncate` class) is a real usability trap for long provider descriptions, and this component avoids it.
  • The four error branches (`appInfoError`, `appParamsError`, `appMetaError`, `useCanAccessAppError`) each return early with the same `AppUnavailable` shape instead of merging into one nested conditional. This keeps every failure path legible on its own, which matters more than the repetition costs here since each error source is genuinely distinct.
  • Putting `<SkipNav>` and `AppDetailStoreCleanup` at the top of the tree, ahead of the conditional `MainNav`/`detailSidebar` swap, keeps the skip-link and cleanup logic running regardless of which nav variant renders. That's the right place for cross-cutting concerns that shouldn't depend on route-specific branching.
  • The decorative add icon (`i-ri-add-line`) is marked `aria-hidden` while the visible "Add" text label sits next to it in its own `<span>`: this is exactly right: screen readers skip the redundant glyph and announce only the meaningful label, instead of reading "image Add" or duplicating the name.

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.