langgenius on GitHub

langgenius/dify

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

30 files reviewed·August 1, 2026

View on GitHub

Low

Design risk in this codebase.

8issues
Serious · top 6 shown below

Top fix

Convert the logout span into a real focusable button element

See the fix

Verdict

Controls here look right but don't behave right: spans posing as buttons, disabled states with no explanation, color-only signaling. The biggest risk is keyboard and screen-reader users hitting dead ends at exactly the moments that matter, like logging out.

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/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/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

web/app/components/app/configuration/config/agent/agent-setting/index.tsx

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

90/100

Accessibility

5 serious
AccessibilitySerious

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

Logout action on a span locks out keyboard users entirely

In the 403 unavailable state, the logout control is a `<span>` with `onClick={backToHome}` and text "userProfile.logout", not a button. It has no tabindex, no keyboard handler, and no focus style, so it is invisible to tab navigation.

Why it matters

A keyboard-only user who lands on this permission-denied screen has no way to log out and retry with a different account, trapping them on a dead end.

Fix

Use a native `<button>` for any element that triggers an action 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="cursor-pointer system-sm-regular text-text-tertiary" onClick={backToHome}>
  {t(($) => $['userProfile.logout'], { ns: 'common' })}
</button>
AccessibilitySerious

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

Drag-active drop zone signals state with color alone

When `dragging` is true, the only feedback is a dashed border and a tinted background on an empty `<div>`; there is no visible text or `aria-live` region announcing that a file drop is active.

Why it matters

A screen reader user dragging a file over this zone gets zero confirmation the drop target is armed, and a color-blind user relying only on the tint may miss the state change too.

Fix

Pair the visual drop-state change with a visually-hidden live region or label so the state is announced, not just shown.

{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 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">
    <span className="sr-only" role="status">Drop file to install plugin</span>
  </div>
)}
AccessibilitySerious

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

Clear button's 14px hit target fails the tap-target minimum

The clear button in the model search input (`aria-label="Clear"`) is sized `size-3.5` (14px) with no padding to extend its hit area beyond the visible icon.

Why it matters

Anyone with reduced fine motor control or using a touch device misses the tap repeatedly, adding friction to a simple clear-search action.

Fix

Keep the visual icon small but pad the interactive button to at least 24px so the hit area covers the WCAG 2.5.5 target size floor.

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"
className="ml-1.5 flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-full 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"
AccessibilitySerious

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

Disabled add button gives no reason it can't be used

When `onAddAccessSubject` is not passed, the fallback renders `<Button variant="primary" size="medium" disabled>` with the icon "i-ri-add-line" and label "operation.add", with no title, tooltip, or aria-describedby explaining the disabled state.

Why it matters

A user staring at a disabled primary-styled add button with no explanation has no way to learn what permission or condition would unblock it, stalling their task.

Fix

Attach a tooltip or aria-describedby text to any disabled action so the reason is discoverable, not just visually implied.

<Button variant="primary" size="medium" disabled>
  <span className="i-ri-add-line size-3.5" aria-hidden />
  <span>{t(($) => $['operation.add'], { ns: 'common' })}</span>
</Button>
<Button variant="primary" size="medium" disabled aria-describedby="add-access-subject-disabled-reason">
  <span className="i-ri-add-line size-3.5" aria-hidden />
  <span>{t(($) => $['operation.add'], { ns: 'common' })}</span>
</Button>
<span id="add-access-subject-disabled-reason" className="sr-only">
  {t(($) => $['accessRule.addDisabledReason'], { ns: 'permission' })}
</span>
AccessibilitySerious

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

App info trigger silently no-ops for users without layout access

`AppInfoTrigger`'s `onClick` checks `appACLCapabilities.canAccessLayout` and calls `setPanelOpen` only if true, but the trigger itself carries no `disabled` prop or visual state change when the capability is false.

Why it matters

A user without layout access clicks the trigger, sees nothing happen, and has no way to tell whether it's broken or restricted, which reads as a bug rather than a permission boundary.

Fix

Pass the capability down as a disabled state on the trigger itself so the control visibly reflects what it can do before the user clicks it.

<AppInfoTrigger
  appDetail={appDetail}
  expand={expand}
  onClick={() => {
    if (appACLCapabilities.canAccessLayout) setPanelOpen((v) => !v)
  }}
/>
<AppInfoTrigger
  appDetail={appDetail}
  expand={expand}
  disabled={!appACLCapabilities.canAccessLayout}
  onClick={() => {
    if (appACLCapabilities.canAccessLayout) setPanelOpen((v) => !v)
  }}
/>
98/100

UX

1 serious
UXSerious

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

Empty access rules state offers text with no path to add a subject

When `userAccessSettings.length === 0`, the table shows only the text "accessRule.noUserAccessSettings" in a plain `<div>`, while the add button above it renders `disabled` since `onAddAccessSubject` is unset in that path.

Why it matters

A user who reaches this empty state has no actionable next step in view, so granting the first access rule requires them to guess where the control lives.

Fix

Give the empty state its own call to action, or make clear why the add control above is disabled, so the next step is visible where the user is looking.

<div className="px-4 py-8 text-center system-sm-regular text-text-tertiary">
  {t(($) => $['accessRule.noUserAccessSettings'], { ns: 'permission' })}
</div>
<div className="flex flex-col items-center gap-2 px-4 py-8 text-center system-sm-regular text-text-tertiary">
  {t(($) => $['accessRule.noUserAccessSettings'], { ns: 'permission' })}
  {onAddAccessSubject && (
    <button type="button" className="system-sm-medium text-text-accent" onClick={onAddAccessSubject}>
      {t(($) => $['operation.add'], { ns: 'common' })}
    </button>
  )}
</div>

Typography

No issues found

Color

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • The four error states (appInfoError, appParamsError, appMetaError, useCanAccessAppError) each render their own AppUnavailable message instead of collapsing into one generic failure screen, which keeps recovery context specific to what actually broke.
  • ScrollAreaViewport sets role='region' with an aria-label tied to the caller's label prop, giving assistive tech a real scrollable landmark instead of an unlabeled generic region.
  • Access rule table rows apply border-t only when index > 0, which avoids a stray top border on the first row without any bespoke first-child override.

Scored August 1, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored July 9, 2026: 59/100. This rescore on v0.0.3: 84/100.

This page is an automated design review of langgenius/dify’s UI code: 30 files read against 309 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.

Or get a design review on every pull requestInstall Rams