toeverything on GitHub

toeverything/affine

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

30 files reviewed·August 1, 2026

View on GitHub

Elevated

Accessibility needs attention.

7issues
Critical & Serious · top 6 shown below

Top fix

Add an accessible name to the color menu trigger button

See the fix

Verdict

A theming UI built for the eye, not the interface: swatches, underlines, and placeholders stand in for labels, names, and table markup. The biggest risk is that core actions like picking a color or saving a collection are invisible or unresponsive to anyone using assistive tech.

Files Rams reviewed

packages/frontend/core/src/desktop/pages/theme-editor/components/color-cell.tsx

packages/frontend/core/src/desktop/pages/theme-editor/components/tree-node.tsx

packages/frontend/core/src/desktop/pages/theme-editor/components/variable-list.tsx

packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx

packages/frontend/core/src/desktop/pages/workspace/layouts/workspace-layout.tsx

packages/frontend/core/src/desktop/pages/workspace/share/share-page.tsx

packages/frontend/core/src/desktop/pages/workspace/trash-page.tsx

packages/frontend/core/src/mobile/pages/workspace/layout.tsx

packages/frontend/component/src/components/affine-other-page-layout/layout.tsx

packages/frontend/component/src/components/auth-components/change-email-page.tsx

packages/frontend/component/src/components/auth-components/change-password-page.tsx

packages/frontend/component/src/components/auth-components/onboarding-page.tsx

packages/frontend/component/src/components/auth-components/set-password-page.tsx

packages/frontend/component/src/components/auth-components/sign-up-page.tsx

packages/frontend/component/src/components/member-components/accept-invite-page.tsx

packages/frontend/component/src/components/member-components/failed-to-send-page.tsx

packages/frontend/component/src/components/member-components/join-failed-page.tsx

packages/frontend/component/src/components/member-components/request-to-join-page.tsx

packages/frontend/component/src/components/member-components/sent-request-page.tsx

packages/frontend/component/src/components/not-found-page/not-found-page.tsx

packages/frontend/core/src/components/page-list/docs/select-page.tsx

packages/frontend/core/src/components/page-list/selector/selector-layout.tsx

packages/frontend/core/src/desktop/pages/theme-editor/components/empty.tsx

packages/frontend/core/src/desktop/pages/theme-editor/components/simple-color-picker.tsx

packages/frontend/core/src/desktop/pages/theme-editor/components/string-cell.tsx

packages/frontend/core/src/modules/open-in-app/views/open-in-app-page.tsx

packages/frontend/core/src/desktop/pages/404/index.tsx

packages/frontend/core/src/desktop/pages/ai-upgrade-success/index.tsx

packages/frontend/core/src/desktop/pages/import-clipper/index.tsx

packages/frontend/core/src/desktop/pages/import-template/index.tsx

88/100

Accessibility

2 critical3 serious
AccessibilityCritical

packages/frontend/core/src/desktop/pages/theme-editor/components/color-cell.tsx:66

Color menu trigger button has no accessible name at all

The `IconButton` that opens the color-edit menu in `color-cell.tsx` renders only `<MoreHorizontalIcon />` with no `aria-label`, no visible text, and no `title`. A screen reader announces it as an unlabeled button.

Why it matters

Screen reader users cannot tell what this control does or that it opens a color-editing menu, so they lose access to the color override feature entirely.

Fix

Give every icon-only trigger an aria-label describing its action.

<IconButton size="14" icon={<MoreHorizontalIcon />} />
<IconButton size="14" icon={<MoreHorizontalIcon />} aria-label="Edit color options" />
AccessibilityCritical

packages/frontend/core/src/desktop/pages/theme-editor/components/color-cell.tsx:55

Custom color input has no label besides placeholder text

The `Input` inside the color menu in `color-cell.tsx` sets `placeholder="Input color"` but no `<label>`, `aria-label`, or `aria-labelledby`. Once a value is typed, the placeholder disappears and the field becomes unidentifiable to assistive tech.

Why it matters

Screen reader users hear an unlabeled text field once content is present, so they can't confirm what they're editing before submitting a color value.

Fix

Add a persistent aria-label to text inputs instead of relying on placeholder text alone.

<Input
  value={inputValue}
  onChange={onInput}
  placeholder="Input color"
/>
<Input
  value={inputValue}
  onChange={onInput}
  placeholder="Input color"
  aria-label="Custom color value"
/>
AccessibilitySerious

packages/frontend/core/src/desktop/pages/theme-editor/components/variable-list.tsx:30

Overridden theme variables are marked only by an underline

In `variable-list.tsx`, the variable name `<li>` gets `textDecoration: 'underline'` when a custom light or dark value exists, and `'none'` otherwise. There's no accompanying text, icon, or aria attribute stating the override status.

Why it matters

Screen reader users get no signal that a variable has been customized, and low-vision users relying on high-contrast modes may not perceive the underline either.

Fix

Pair any color- or style-only status signal with a visible text or icon indicator.

<li
  style={{
    textDecoration:
      customTheme?.light?.[variable.variableName] ||
      customTheme?.dark?.[variable.variableName]
        ? 'underline'
        : 'none',
  }}
>
  {variable.name}
</li>
<li
  style={{
    textDecoration:
      customTheme?.light?.[variable.variableName] ||
      customTheme?.dark?.[variable.variableName]
        ? 'underline'
        : 'none',
  }}
>
  {variable.name}
  {(customTheme?.light?.[variable.variableName] ||
    customTheme?.dark?.[variable.variableName]) && (
    <span aria-label="customized"> (custom)</span>
  )}
</li>
AccessibilitySerious

packages/frontend/core/src/desktop/pages/theme-editor/components/variable-list.tsx:19

Variable table header uses plain lists, not real table semantics

The column header row "Name", "Light", "Dark" and every data row in `variable-list.tsx` are built from `<ul>`/`<li>` rather than `<table>`/`<th>`/`<td>`. There is no `role="table"` or `role="columnheader"` either.

Why it matters

Assistive tech can't announce column/row relationships, so a screen reader user tabbing through variables has no way to know which value belongs to Light vs Dark.

Fix

Use table markup or explicit table/grid roles for tabular data so column relationships are announced.

<header>
  <ul className={styles.row}>
    <li>Name</li>
    <li>Light</li>
    <li>Dark</li>
  </ul>
</header>
<header role="row">
  <div className={styles.row} role="columnheader">Name</div>
  <div className={styles.row} role="columnheader">Light</div>
  <div className={styles.row} role="columnheader">Dark</div>
</header>
AccessibilitySerious

packages/frontend/core/src/desktop/pages/theme-editor/components/color-cell.tsx:35

Empty custom-color swatch gives no cue that no override exists

The second row in `color-cell.tsx` renders a color block with `backgroundColor: custom` and a text value `{custom}`. When `custom` is `undefined`, both render empty with only the invisible `data-empty` attribute marking the state, no visible text says 'no override'.

Why it matters

Sighted users see a blank swatch and can't tell if that's an actual transparent color or simply an absent override, which makes the custom/default distinction ambiguous.

Fix

Render an explicit fallback label when the custom value is absent instead of an empty swatch.

<div data-empty={!custom} data-custom className={styles.colorCellRow}>
  <div
    className={styles.colorCellColor}
    style={{ backgroundColor: custom }}
  />
  <div className={styles.colorCellValue}>{custom}</div>
</div>
<div data-empty={!custom} data-custom className={styles.colorCellRow}>
  <div
    className={styles.colorCellColor}
    style={{ backgroundColor: custom }}
  />
  <div className={styles.colorCellValue}>{custom ?? 'No custom color'}</div>
</div>
98/100

UX

1 serious
UXSerious

packages/frontend/core/src/desktop/pages/workspace/all-page/all-page.tsx:320

Save button has no disabled or loading state during collection creation

The "Save" button (`t['save']()`) triggers `handleSaveFilters`, which opens a prompt modal whose `onConfirm` calls `collectionService.createCollection` and `pinnedCollectionService.addPinnedCollection`. Neither the confirm button nor the trigger disables while the create call is in flight.

Why it matters

A user who double-clicks or clicks again before the modal closes can fire `createCollection` twice, producing duplicate pinned collections from one save action.

Fix

Disable the confirm action while an async create/save request is pending.

onConfirm(name) {
  const id = collectionService.createCollection({
    name,
    rules: {
      filters: tempFilters ?? [],
    },
  });
  pinnedCollectionService.addPinnedCollection({
    collectionId: id,
    index: pinnedCollectionService.indexAt('after'),
  });
  setTempFilters(null);
  setSelectedCollectionId(id);
},
onConfirm(name) {
  if (isSaving) return;
  setIsSaving(true);
  const id = collectionService.createCollection({
    name,
    rules: {
      filters: tempFilters ?? [],
    },
  });
  pinnedCollectionService.addPinnedCollection({
    collectionId: id,
    index: pinnedCollectionService.indexAt('after'),
  });
  setTempFilters(null);
  setSelectedCollectionId(id);
  setIsSaving(false);
},

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

  • Deriving `isLeaf` from `!node.children && node.variables` and branching the click behavior cleanly between select-and-expand keeps the recursive tree logic easy to follow.
  • The Cancel/Save button pair correctly differentiates by variant (plain vs default), giving the save action clear primary weight without extra styling overrides.
  • Separating noPermission and loadFailed into distinct PageNotFound states shows real attention to differentiating error causes for the user.
  • Each color swatch pairs its color block with a visible hex value in colorCellValue, so color is never the sole indicator of the value.

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: 59/100.

This page is an automated design review of toeverything/affine’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