windmill-labs/windmill
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Interaction sequencing is thoughtful, autofocus, Enter-to-submit, clear Create/Cancel, but the file grid abandons semantic structure entirely, leaving screen reader users without row or column context. The biggest risk is accessibility debt hiding behind polished visuals: a stray blue fails contrast and inputs lack labels.
Files Rams reviewed
frontend/src/lib/components/tutorials/app/BackgroundRunnablesTutorial.svelte
frontend/src/lib/components/tutorials/app/ConnectionTutorial.svelte
ai_evals/fixtures/frontend/app/initial/file_manager/frontend/components/FileItem.tsx
ai_evals/fixtures/frontend/app/initial/file_manager/frontend/components/FileList.tsx
ai_evals/fixtures/frontend/app/initial/file_manager/frontend/components/FolderTree.tsx
ai_evals/fixtures/frontend/app/initial/file_manager/frontend/components/Toolbar.tsx
ai_evals/fixtures/frontend/app/initial/shopping_cart/frontend/components/Cart.tsx
frontend/src/lib/components/tutorials/app/ExpressionEvaluationTutorial.svelte
ai_evals/fixtures/frontend/app/initial/file_manager/frontend/components/Breadcrumb.tsx
ai_evals/fixtures/frontend/app/initial/shopping_cart/frontend/components/ProductCard.tsx
ai_evals/fixtures/frontend/app/initial/shopping_cart/frontend/components/ProductList.tsx
ai_evals/fixtures/frontend/app/initial/file_manager/frontend/index.tsx
ai_evals/fixtures/frontend/app/initial/inventory_tracker/frontend/index.tsx
ai_evals/fixtures/frontend/app/initial/recipe_book/frontend/index.tsx
ai_evals/fixtures/frontend/app/initial/session_id_chat/frontend/index.tsx
ai_evals/fixtures/frontend/app/initial/shopping_cart/frontend/index.tsx
ai_evals/fixtures/frontend/app/initial/token_heavy_context/frontend/components/ReferencePanel.tsx
frontend/src/lib/components/AIAgentLogViewer.svelte
frontend/src/lib/components/AIProviderPicker.svelte
frontend/src/lib/components/AIReasoningEffortPicker.svelte
frontend/src/lib/components/AddUser.svelte
frontend/src/lib/components/ApiConnectForm.svelte
frontend/src/lib/components/AppConnectDrawer.svelte
frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte
frontend/src/lib/components/ArgEnum.svelte
frontend/src/lib/components/ArgInfo.svelte
frontend/src/lib/components/ArrayTypeNarrowing.svelte
frontend/src/lib/components/AssignableTags.svelte
frontend/src/lib/components/AssignableTagsInner.svelte
frontend/src/lib/components/Auth0Setting.svelte
Accessibility
White text on the blue fill fails AA contrast at this size
The "Create" and "New Folder" buttons render white text (#ffffff) on `bg-blue-500` (#3b82f6). At normal text weight and size that pair computes to 3.68:1.
Why it matters
WCAG AA requires 4.5:1 for normal-size text; this button falls short for users with low vision, and it's the primary action in the toolbar.
Fix
Raise text-on-fill contrast to at least 4.5:1 for normal-size button labels.
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"className="px-3 py-1 bg-gray-900 text-white rounded hover:bg-gray-800"File list built from plain divs loses row and column structure for screen readers
The header row (`Name`, `Type`, `Modified`, `Actions`) and each file row below it are plain `<div>` elements laid out with `grid-cols-12`. No `<table>`/`<ul>` semantics or ARIA roles (`role="table"`, `role="row"`, `role="columnheader"`) mark this as a structured listing.
Why it matters
Screen reader users navigating by table or list get no row count, column count, or column-header association here, so this reads as four unrelated lines of text instead of a data grid, even though sighted users clearly see aligned columns.
Fix
Add table/row/columnheader roles (or swap to a native table) so the visual grid has matching semantic structure.
<div className="grid grid-cols-12 gap-4 px-4 py-2 border-b bg-gray-50 font-medium text-sm text-gray-600">
<div className="col-span-6">Name</div>
<div className="col-span-2">Type</div>
<div className="col-span-2">Modified</div>
<div className="col-span-2">Actions</div>
</div><div className="grid grid-cols-12 gap-4 px-4 py-2 border-b bg-gray-50 font-medium text-sm text-gray-600" role="row">
<div className="col-span-6" role="columnheader">Name</div>
<div className="col-span-2" role="columnheader">Type</div>
<div className="col-span-2" role="columnheader">Modified</div>
<div className="col-span-2" role="columnheader">Actions</div>
</div>'New Folder' hit area falls well under the 44px touch target
The primary "New Folder" button uses `py-1` (4px top/bottom padding) with default text line-height, putting the tappable height near 24-28px.
Why it matters
This is the toolbar's main entry point for creating content; on a touch surface a target this small increases mis-taps for users with motor impairments.
Fix
Give primary actions at least 44px of tap height, using vertical padding or a min-height utility.
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 flex items-center gap-2"className="px-3 py-2.5 bg-gray-900 text-white rounded hover:bg-gray-800 flex items-center gap-2"Folder name input has no accessible label, only a placeholder
The `<input type="text">` for the new folder relies on `placeholder="Folder name"` with no `<label>` or `aria-label`.
Why it matters
Placeholder text disappears the moment the user types and screen readers don't reliably announce it as the field's purpose, so users of assistive tech get an unlabeled text box.
Fix
Give every form input a persistent accessible name via aria-label or a visually-hidden label.
<input
type="text"
value={folderName}
onChange={(e) => setFolderName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
placeholder="Folder name"
className="border rounded px-3 py-1"
autoFocus
/><input
type="text"
value={folderName}
onChange={(e) => setFolderName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
placeholder="Folder name"
aria-label="Folder name"
className="border rounded px-3 py-1"
autoFocus
/>UX
Empty folder state is a dead end with no path forward
When `files.length === 0`, the component renders a single gray line: "This folder is empty". There is no button, link, or hint pointing the user to their next action.
Why it matters
This is the first thing a user sees in a brand-new or cleared-out folder. Landing on a dead end here means the user has to hunt for the create/upload control elsewhere in the UI instead of being handed it at the moment they need it.
Fix
Give the empty state a primary action tied to folder creation, gated behind an optional callback prop.
return (
<div className="text-center text-gray-500 py-8">This folder is empty</div>
)return (
<div className="text-center py-12">
<p className="text-gray-500 mb-3">This folder is empty</p>
{onCreateFolder && (
<button
type="button"
onClick={onCreateFolder}
className="text-sm font-medium text-gray-900 underline underline-offset-2"
>
Create a folder
</button>
)}
</div>
)Component has no loading or error branch between empty and full states
`FileList` only handles two states: `files.length === 0` and a populated array via `sortedFiles.map`. There is no branch for a fetch-in-progress or fetch-failed state, even though `files` is the kind of prop that typically arrives from an async load.
Why it matters
If the parent ever passes an empty array while data is still loading, or after a failed fetch, users see the same "This folder is empty" message as a truly empty folder, so a fetch error looks identical to having no files.
Fix
Accept explicit loading/error props (or a status enum) and render a skeleton or retry state before falling through to the empty-folder message.
if (files.length === 0) {
return (
<div className="text-center text-gray-500 py-8">This folder is empty</div>
)
}if (isLoading) {
return <div className="text-center text-gray-500 py-8">Loading files…</div>
}
if (error) {
return (
<div className="text-center text-gray-500 py-8">
Couldn't load this folder. <button type="button" onClick={onRetry} className="underline">Try again</button>
</div>
)
}
if (files.length === 0) {
return (
<div className="text-center text-gray-500 py-8">This folder is empty</div>
)
}Typography
Color
Spacing
Components
Motion
Craft
Working well
- Sorting folders before files, then alphabetically within each group (`if (a.type === 'folder' && b.type !== 'folder') return -1`), is the right call for a file browser: users scan for containers first and files second, so grouping by type before name keeps the list predictable regardless of how items are named.
- The folder-name input carries `autoFocus`, so the moment 'New Folder' is clicked, the cursor is already in the field ready to type. That's the right call: the user came here to name something, and reaching for the mouse to click into the only input would break the flow at the exact moment intent is highest.
- `FileListProps` types every callback explicitly (`onDelete: (item: FileItem) => void`, etc.) instead of leaving them as untyped functions. This is what makes the component safe to reuse: any caller gets a compile error the moment they pass the wrong shape, instead of a silent runtime failure.
- The header grid (`grid-cols-12`, `col-span-6`, `col-span-2`) sticks entirely to Tailwind's standard scale with no arbitrary bracket values, so the column layout stays predictable and easy to adjust from one shared system rather than hand-tuned pixel widths.
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.