toeverything/affine
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
Elevated
Accessibility needs attention.
More findings
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
Accessibility
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" />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"
/>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>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>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>UX
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
Color
Spacing
Components
Motion
Craft
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.