appsmithorg/appsmith
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
29 files reviewed·July 9, 2026
More findings
Verdict
Design tokens are used consistently for color and spacing, but keyboard and screen-reader access keeps getting left out of the same components. A styled div standing in for a real navigation tab is the biggest liability, since it locks out keyboard users entirely.
Files Rams reviewed
app/client/src/pages/AppIDE/components/AppSettings/components/EmbedSettings/index.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/NavigationSettings/index.tsx
app/client/src/pages/AppIDE/components/PageList/ContextMenu/SetAsHomePage.tsx
app/client/src/pages/AppIDE/components/WidgetAdd/index.tsx
app/client/src/pages/AppIDE/layouts/components/EditorTabs/index.tsx
app/client/src/pages/AppIDE/layouts/components/Header/index.tsx
app/client/src/pages/AppViewer/Navigation/components/MenuItem/index.tsx
app/client/src/pages/Editor/gitSync/components/GitChangesList/index.tsx
app/client/src/ce/pages/AppIDE/components/AppIDEModals.tsx
app/client/src/ce/pages/AppIDE/components/QueryAdd/useGroupedAddQueryOperations.tsx
app/client/src/pages/AppIDE/components/AppPluginActionEditor/components/ContextMenuItems/ConvertToModule.tsx
app/client/src/pages/AppIDE/components/AppPluginActionEditor/components/ContextMenuItems/Copy.tsx
app/client/src/pages/AppIDE/components/AppPluginActionEditor/components/ContextMenuItems/CopyToApp.tsx
app/client/src/pages/AppIDE/components/AppPluginActionEditor/components/ContextMenuItems/Delete.tsx
app/client/src/pages/AppIDE/components/AppPluginActionEditor/components/ContextMenuItems/Move.tsx
app/client/src/pages/AppIDE/components/AppPluginActionEditor/components/ContextMenuItems/ShowBindings.tsx
app/client/src/pages/AppIDE/components/AppPluginActionEditor/components/ConvertToModule/ConvertToModuleCallout.tsx
app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/DraggablePageList.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/EmbedSettings/MakeApplicationForkable.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/ImportAppSettings.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/NavigationSettings/ButtonGroupSetting.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/NavigationSettings/ColorStyleIcon.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/NavigationSettings/ImageInput.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/NavigationSettings/LogoInput.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/NavigationSettings/SwitchSetting.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/NavigationSettings/SwitchSettingForLogoConfiguration.tsx
app/client/src/pages/AppIDE/components/AppSettings/components/StaticURLConfirmationModal.tsx
app/client/src/pages/AppIDE/components/GitModals.tsx
Accessibility
Page navigation tab is a styled div, not reachable by keyboard
`StyledMenuItem` renders as a styled component with `onClick={handleClick}` and no `role`, `tabIndex`, or `onKeyDown`. There is no `<button>` or `<a>` in the render tree for this nav item.
Why it matters
Keyboard-only and screen-reader users cannot tab to or activate page navigation items in the app's top nav, which blocks navigation entirely for that group of users.
Fix
Render the interactive nav item as a native <button> (or add role='button', tabIndex={0}, and an Enter/Space key handler) so it is keyboard operable.
<StyledMenuItem
borderRadius={borderRadius}
className={`t--page-switch-tab ${isActive ? "is-active" : ""}`}
navColorStyle={navColorStyle}
onClick={handleClick}
primaryColor={primaryColor}
><StyledMenuItem
as="button"
type="button"
borderRadius={borderRadius}
className={`t--page-switch-tab ${isActive ? "is-active" : ""}`}
navColorStyle={navColorStyle}
onClick={handleClick}
primaryColor={primaryColor}
aria-current={isActive ? "page" : undefined}
>Section header renders as a paragraph, breaking document structure
The section title "In-app embed setting" (createMessage(IN_APP_EMBED_SETTING.sectionContentHeader)) is rendered inside `Title`, a `styled.p`. It's the only header-like text in this panel but carries no heading semantics.
Why it matters
Screen reader users navigate settings panels by jumping between headings. A `<p>` styled to look like a title is invisible to that navigation, so assistive tech users can't jump straight to this section or tell it apart from body copy.
Fix
Use a semantic heading element for section titles instead of a styled paragraph.
const Title = styled.p`
font-size: var(--ads-v2-font-size-4);
line-height: 1.2rem;
font-weight: var(--ads-v2-font-weight-bold);
color: var(--ads-v2-color-fg-emphasis);
`;const Title = styled.h3`
font-size: var(--ads-v2-font-size-4);
line-height: 1.2;
font-weight: var(--ads-v2-font-weight-bold);
color: var(--ads-v2-color-fg-emphasis);
margin: 0;
`;Icon-only list toggle button ships no accessible name or state
`EntityListButton` renders with only `data-testid="t--list-toggle"`, `isSelected`, and `onClick`. No `aria-label` or `aria-expanded` is passed at the call site, even though it toggles the `List` panel open and closed.
Why it matters
Screen reader users hear an unlabeled button with no indication it expands a panel, so they can't tell what it does or whether the list is currently open.
Fix
Give icon-only toggle controls an aria-label describing the action and aria-expanded reflecting the open state.
<EntityListButton
data-testid="t--list-toggle"
isSelected={isListViewActive}
onClick={handleHamburgerClick}
/><EntityListButton
aria-expanded={isListViewActive}
aria-label="Toggle file list"
data-testid="t--list-toggle"
isSelected={isListViewActive}
onClick={handleHamburgerClick}
/>Breadcrumb slash separator is announced literally by screen readers
Between the workspace `Link` and `EditorName`, the raw text node `{"/"}` sits unhidden in the DOM as a breadcrumb separator.
Why it matters
Screen readers announce this as the word "slash" between the workspace name and application name, adding noise to every header read instead of a clean pause between the two labels.
Fix
Mark decorative separators aria-hidden so assistive tech skips them.
{"/"}<span aria-hidden="true">/</span>Spacing
Settings list has no vertical rhythm between up to seven stacked sections
The root return renders `<div className="px-4">` wrapping `SwitchSetting`, two `ButtonGroupSetting` blocks, `SwitchSettingForLogoConfiguration`, `LogoInput`, and another `SwitchSetting`, with no `space-y-*` or `gap` on the container.
Why it matters
Spacing between sections depends entirely on whatever margin each child component happens to define internally, so adding or removing a conditional block (like the nav-style options that only show for TOP orientation) can visibly shift density elsewhere in the panel without anyone touching those components.
Fix
Control vertical rhythm at the container level with a spacing-scale utility so section spacing stays consistent regardless of which child sections render.
return (
<div className="px-4">return (
<div className="px-4 space-y-4">Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeUX
Tabs render with no loading, empty, or error branch for either list
`WidgetsList` and `UIModulesList` are rendered directly inside their `TabPanel`s with no conditional handling visible in this file: - `<TabPanel value="widgets"><WidgetsList .../></TabPanel>` - `<TabPanel value="modules"><UIModulesList .../></TabPanel>` If either list is empty (no widgets match a search, no modules exist) or fails to load, this component has no visible fallback path of its own.
Why it matters
If the child lists don't handle their own empty/error state, users switching to the 'Modules' tab with nothing configured see a blank panel with no explanation and no next step, which reads as broken rather than intentionally empty.
Fix
Confirm each list component renders a designed empty and error state, or pass a fallback prop from this container so neither tab can render blank.
<TabPanel className="TabsContent" value="modules">
<UIModulesList focusSearchInput={props.focusSearchInput} />
</TabPanel><TabPanel className="TabsContent" value="modules">
<UIModulesList
emptyState={<ModulesEmptyState />}
focusSearchInput={props.focusSearchInput}
/>
</TabPanel>Typography
Color
Components
Motion
Craft
Working well
- `hideAdd` correctly gates the add-tab affordance on three independent conditions (`Add` mode, empty file list, and `canCreateActions` permission) rather than just one. Folding the permission check into the same boolean as the state checks keeps a single source of truth for whether the action is even offered, instead of showing a button that then fails silently on click.
- Swapping `HeaderTitleComponent` on `appState` with a `switch` and a distinct `key={appState}` per case is a clean pattern: it guarantees React treats each editor mode as a fresh title rather than patching a stale one mid-transition, avoiding stale-title flicker when switching between Data, Editor, and Settings.
- The nav-style `ButtonGroupSetting` only renders when `orientation === TOP`, and individual options are marked `hidden` based on orientation rather than being removed and re-added. This progressive disclosure keeps the panel from showing options that don't apply to the current configuration.
- The component composes entirely from design-system primitives (`EntityTabsHeader`, `EntityListButton`, `EntityTabBar`, `EditableTab`) with zero inline styles or hardcoded colors. That discipline is why this file will survive a theme change untouched while hand-rolled tab bars usually break.
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.