tldraw/tldraw
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·August 1, 2026
Elevated
Design risk in this codebase.
More findings
Verdict
Admin tooling that works but never quite feels considered, from browser-native confirms to loading states that silently vanish. The biggest risk is destructive actions relying on confirm() instead of the app's own review flow.
Files Rams reviewed
apps/dotcom/client/src/components/ErrorPage/ErrorPage.tsx
packages/tldraw/src/lib/ui/components/primitives/layout.tsx
apps/docs/app/(docs)/[...slug]/page.tsx
apps/docs/app/layout.tsx
apps/dotcom/client/src/pages/admin/index.tsx
apps/dotcom/client/src/tla/pages/local-file-index.tsx
templates/chat/src/app/layout.tsx
templates/chat/src/app/page.tsx
templates/nextjs/src/app/layout.tsx
templates/nextjs/src/app/page.tsx
apps/dotcom/client/src/components/LoginRedirectPage/LoginRedirectPage.tsx
apps/dotcom/client/src/pages/admin/FlagsSection.tsx
apps/dotcom/client/src/pages/admin/SystemSection.tsx
apps/dotcom/client/src/pages/admin/UsersSection.tsx
apps/dotcom/client/src/pages/admin/admin.module.css
apps/dotcom/client/src/pages/admin/shared.tsx
apps/dotcom/client/src/pages/dev-browser-run-thumbnail.tsx
apps/dotcom/client/src/pages/dev-reset-local-state.tsx
apps/dotcom/client/src/tla/pages/file-history-snapshot.tsx
apps/dotcom/client/src/tla/pages/file-history.tsx
apps/dotcom/client/src/tla/pages/file-pierre-history-snapshot.tsx
apps/dotcom/client/src/tla/pages/file-pierre-history.tsx
apps/dotcom/client/src/tla/pages/file.tsx
apps/dotcom/client/src/tla/pages/import.tsx
apps/dotcom/client/src/tla/pages/invite.tsx
apps/dotcom/client/src/tla/pages/legacy-history-snapshot.tsx
apps/dotcom/client/src/tla/pages/legacy-history.tsx
apps/dotcom/client/src/tla/pages/legacy-readonly-old.tsx
apps/dotcom/client/src/tla/pages/legacy-readonly.tsx
apps/dotcom/client/src/tla/pages/legacy-room.tsx
UX
apps/dotcom/client/src/tla/pages/local-file-index.tsx:24
Delete action gated by native confirm() instead of app's own dialog
The "delete" button in the local files list calls `confirm('Are you 100% sure you want to delete this file? This action cannot be undone.')` directly. This is the only confirmation step before an irreversible IndexedDB delete, and it bypasses whatever dialog system the rest of the app uses.
Why it matters
Native confirm() dialogs can be suppressed by the browser (repeated dialogs, embedded contexts) and offer no styling, focus management, or accessible labeling consistent with the rest of the product, so a destructive delete can proceed with a confirmation step the user never actually saw.
Fix
Replace native confirm() with the app's accessible confirmation dialog component so destructive actions get consistent, guaranteed confirmation UI.
if (confirm('Are you 100% sure you want to delete this file? This action cannot be undone.')) {
await deleteDB(STORE_PREFIX + persistenceKey)
navigate('.', { replace: true })
}const confirmed = await showAppConfirmDialog({
title: 'Delete file',
message: 'Are you sure you want to delete this file? This action cannot be undone.',
})
if (confirmed) {
await deleteDB(STORE_PREFIX + persistenceKey)
navigate('.', { replace: true })
}apps/dotcom/client/src/pages/admin/index.tsx:19
Admin route redirects home before user auth state finishes loading
`Component` in admin/index.tsx checks `if (!user?.isTldraw) return <Navigate to="/" replace />` immediately after calling `useTldrawCurrentUser()`, with no check for a loading state.
Why it matters
If the hook returns undefined while the user fetch is still in flight, a legitimate admin gets redirected to "/" before their auth resolves, forcing a reload or re-navigation to reach admin tools.
Fix
Check an explicit loading state from the auth hook and defer the redirect decision until that state resolves.
const user = useTldrawCurrentUser()
const { section } = useParams()
if (!user?.isTldraw) {
return <Navigate to="/" replace />
}const { user, isLoading } = useTldrawCurrentUser()
const { section } = useParams()
if (isLoading) {
return <div>Loading...</div>
}
if (!user?.isTldraw) {
return <Navigate to="/" replace />
}apps/dotcom/client/src/pages/admin/SystemSection.tsx:39
System health section shows nothing while data is still loading
The "System health" section only renders `error` or `replicatorData` once the fetch resolves; there is no loading state tracked or shown between the `fetch('/api/app/admin/replicator')` call and its resolution.
Why it matters
Admins loading the page see an empty section with just the heading until the request finishes, unable to tell whether the section is loading, broken, or simply has nothing to show.
Fix
Add an explicit loading state that renders visible feedback while the fetch is in flight.
<section className={styles.adminSection}>
<h3 className={styles.sectionTitle}>System health</h3>
{error && <div className={styles.errorMessage}>{error}</div>}
{replicatorData && <StructuredDataDisplay data={replicatorData} />}
</section><section className={styles.adminSection}>
<h3 className={styles.sectionTitle}>System health</h3>
{isLoading && <p>Loading system health...</p>}
{error && <div className={styles.errorMessage} role="alert">{error}</div>}
{replicatorData && <StructuredDataDisplay data={replicatorData} />}
</section>apps/dotcom/client/src/pages/admin/FlagsSection.tsx:216
Percentage input forces a full keyboard for a 0-100 number entry
The percentage flag input is `<input type="text" value={pct} onChange={...} ...>` with no `inputMode` or `pattern` set, even though the onChange handler clamps the value to a 0-100 number.
Why it matters
On mobile, admins editing a rollout percentage get the full alphanumeric keyboard instead of a numeric one, slowing down what should be a quick numeric edit to a live production flag.
Fix
Add inputMode="numeric" and a digit pattern to text inputs that only accept numbers, so mobile keyboards default to digits.
<input
type="text"
value={pct}
onChange={(e) => {
const n = Number(e.target.value)
if (!Number.isNaN(n)) setPct(Math.max(0, Math.min(100, n)))
}}
disabled={isSaving || !flagValue.enabled}
className={styles.searchInput}
style={{ width: 60 }}
/><input
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={pct}
onChange={(e) => {
const n = Number(e.target.value)
if (!Number.isNaN(n)) setPct(Math.max(0, Math.min(100, n)))
}}
disabled={isSaving || !flagValue.enabled}
className={styles.searchInput}
style={{ width: 60 }}
/>Accessibility
apps/dotcom/client/src/tla/pages/local-file-index.tsx:46
Delete button's zero padding shrinks its tap target below usable size
The delete `<button>` next to each file link is styled with `style={{ padding: 0 }}`, giving it a hit area barely larger than the word "delete" itself.
Why it matters
A hit area well under the 44x44px accessible minimum increases mis-taps on touch devices and for motor-impaired users, which is especially risky on a destructive, irreversible action.
Fix
Give interactive controls a minimum comfortable hit area even when visually compact, using padding rather than zero.
<button onClick={() => onDelete(persistenceKey)} style={{ padding: 0 }}>
delete
</button><button onClick={() => onDelete(persistenceKey)} style={{ padding: '8px 12px' }}>
delete
</button>Get this score on every PR.
Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.
Install Rams freeComponents
apps/dotcom/client/src/pages/admin/FlagsSection.tsx:200
Percentage flag label re-implements a class that already exists
The `<label>` for each percentage flag hardcodes `style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}`, which duplicates the layout already defined on `.featureFlagLabel` in admin.module.css, and this label is nested one level inside a div already using that class.
Why it matters
Any future edit to `.featureFlagLabel` in admin.module.css won't apply to this inline copy, so this specific label's layout silently drifts out of sync with every other flag label over time, growing maintenance cost.
Fix
Reuse the existing .featureFlagLabel class on this element instead of re-declaring its layout inline.
<label
htmlFor={flagName}
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}
><label htmlFor={flagName} className={styles.featureFlagLabel}>Typography
Color
Spacing
Motion
Craft
Working well
- The admin sidebar's active state is computed through NavLink's `isActive` callback (`isActive ? styles.adminSidebarItemActive : styles.adminSidebarItem`) rather than manual path string matching, so it stays correct automatically as routes change.
- The local-file link's inline color still resolves through `var(--tl-color-primary)` rather than a hardcoded hex, keeping it tied to the theme token even though it's set inline.
- The percentage flag's save button is disabled until `pct !== currentPct`, which prevents pointless no-op writes to a flag that affects all users immediately.
Scored August 1, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored July 14, 2026: 96/100. This rescore on v0.0.3: 59/100.
This page is an automated design review of tldraw/tldraw’s UI code: 30 files read against 291 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.