langgenius/dify
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
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
Accessibility
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>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>
)}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>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"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()}
>Spacing
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
Color
Components
Motion
UX
Craft
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.