skyvern-ai/skyvern
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 9, 2026
More findings
Verdict
Solid form plumbing keeps getting undercut by the same two unfixed details everywhere: icon-only buttons with no accessible name, and animations that never stop or check contrast. Credit where due, the underlying architecture (resize logic, form state) is genuinely thoughtful, but polish gaps repeat across files instead of being fixed once.
Files Rams reviewed
skyvern-frontend/src/components/PageLayout.tsx
skyvern-frontend/src/components/AnimatedWave.tsx
skyvern-frontend/src/components/ApiWebhookActionsMenu.tsx
skyvern-frontend/src/components/AutoResizingTextarea/AutoResizingTextarea.tsx
skyvern-frontend/src/components/AzureClientSecretCredentialTokenForm.tsx
skyvern-frontend/src/components/BitwardenCredentialForm.tsx
skyvern-frontend/src/components/ClearCredentialDialog.tsx
skyvern-frontend/src/components/CopyApiCommandDropdown.tsx
skyvern-frontend/src/components/CustomCredentialServiceConfigForm.tsx
skyvern-frontend/src/components/DataSchemaInputGroup/WorkflowDataSchemaInputGroup.tsx
skyvern-frontend/src/components/DeleteConfirmationDialog.tsx
skyvern-frontend/src/components/EngineSelector.tsx
skyvern-frontend/src/components/FileUpload.tsx
skyvern-frontend/src/components/Flippable.tsx
skyvern-frontend/src/components/GeoTargetSelector.tsx
skyvern-frontend/src/components/GlobalNotificationListener.tsx
skyvern-frontend/src/components/ImprovePrompt.tsx
skyvern-frontend/src/components/KeyValueInput.tsx
skyvern-frontend/src/components/ModelSelector.tsx
skyvern-frontend/src/components/NavLinkGroup.tsx
skyvern-frontend/src/components/NoticeMe.tsx
skyvern-frontend/src/components/OnePasswordTokenForm.tsx
skyvern-frontend/src/components/Orgwalled.tsx
skyvern-frontend/src/components/ParameterAutocompleteDropdown.tsx
skyvern-frontend/src/components/ParameterGhostText.tsx
skyvern-frontend/src/components/ProxySelector.tsx
skyvern-frontend/src/components/PushTotpCodeForm.tsx
skyvern-frontend/src/components/RadialMenu.tsx
skyvern-frontend/src/components/RotateThrough.tsx
skyvern-frontend/src/components/SelectionBar.tsx
UX
Clipboard copy failures leave the user with no feedback at all
Both "Copy cURL (Unix/Linux/macOS)" and "Copy PowerShell (Windows)" call `copyText(...).then(...)` with no `.catch()`. If the clipboard write throws (denied permission, insecure context, unsupported browser), the promise rejects silently and no toast fires.
Why it matters
The user clicks the menu item, sees no toast, and has no way to know whether the copy worked or failed. They either assume it worked and paste garbage, or retry the same broken action indefinitely.
Fix
Add a `.catch()` that shows an error toast so failure is as visible as success.
const { curl } = generateApiCommands(getOptions());
copyText(curl).then(() => {
toast({
variant: "success",
title: "Copied to Clipboard",
description:
"The cURL command has been copied to your clipboard.",
});
});const { curl } = generateApiCommands(getOptions());
copyText(curl)
.then(() => {
toast({
variant: "success",
title: "Copied to Clipboard",
description:
"The cURL command has been copied to your clipboard.",
});
})
.catch(() => {
toast({
variant: "destructive",
title: "Copy Failed",
description: "Couldn't copy the command. Try again.",
});
});Resize logic has no guard against runaway height on huge pasted content
`measureAndSize` sets `style.height` directly to `scrollHeight + 2` with no upper bound. A user pasting a very large block of text (logs, JSON, a long prompt) grows the textarea to match the full content height with nothing capping it.
Why it matters
An unbounded textarea can push surrounding page content far below the fold or off-screen inside a modal/drawer, forcing users to scroll the whole page just to see the rest of a form.
Fix
Cap the computed height with a max-height and let overflow-y switch to auto once the cap is hit, rather than growing without limit.
if (lastHeightRef.current !== newHeight) {
lastHeightRef.current = newHeight;
textareaElement.style.height = newHeight;const cappedHeight = `${Math.min(measured + 2, 480)}px`;
if (lastHeightRef.current !== cappedHeight) {
lastHeightRef.current = cappedHeight;
textareaElement.style.height = cappedHeight;
textareaElement.style.overflowY = measured + 2 > 480 ? "auto" : "hidden";Confirm button stays clickable while clearing, enabling double-submit
The confirm button at the bottom of the dialog shows "Clearing..." during the async call but its `disabled` prop is bound only to the `disabled` prop, not `isPending`: - Cancel button: `disabled={isPending}` (correctly locked during the operation) - Confirm button: `disabled={disabled}` (not locked during the operation) A user can click the destructive confirm button repeatedly while `onConfirm()` is in flight.
Why it matters
Repeated clicks during a pending destructive action can fire `onConfirm` multiple times, risking duplicate clear requests or race conditions in whatever credential-clearing logic sits upstream.
Fix
Disable the action button whenever the async operation is pending, not only when the caller disables it.
disabled={disabled}disabled={disabled || isPending}Accessibility
Icon-only password toggle has no accessible name or pressed state
The visibility toggle inside the "Client Secret" field renders only `<EyeOpenIcon />` / `<EyeClosedIcon />` with no `aria-label` and no `aria-pressed`.
Why it matters
Screen reader users hear an unlabeled button with no indication of what it does or whether the secret is currently shown, so they can't safely reveal or re-hide a credential value.
Fix
Give icon-only toggle controls a stable aria-label plus aria-pressed reflecting the visibility state.
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={toggleClientSecretVisibility}
disabled={isLoading || isMutating}
><Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={toggleClientSecretVisibility}
disabled={isLoading || isMutating}
aria-label="Show client secret"
aria-pressed={showClientSecret}
>Want this on your repo?
Rams reviews your next PR automatically and posts inline fix suggestions.
Review my public repo for freeMotion
Wave animation loops forever with no reduced-motion escape hatch
The `@keyframes wave` block and the inline `animationIterationCount: "infinite"` on each character span run permanently with no `prefers-reduced-motion` guard anywhere in the file.
Why it matters
Users with vestibular disorders who set reduced-motion at the OS level still get the full bouncing animation on every character, every time this component renders, with no way to opt out.
Fix
Wrap the keyframe declaration in @media (prefers-reduced-motion: no-preference) so the animation only runs when the user hasn't opted out.
.animate-wave {
animation-name: wave;
}
`}</style> @media (prefers-reduced-motion: no-preference) {
.animate-wave {
animation-name: wave;
}
}
`}</style>Color
Status label 'Active' fails contrast at 3.29:1 against white
The status `<span>` renders "Active" in `text-green-600` (#16a34a) at `text-sm` on a white card background. Computed contrast is ~3.29:1.
Why it matters
Falls below WCAG AA's 4.5:1 threshold for small text, so users with low vision struggle to read whether their Bitwarden credential is active.
Fix
Darken the green to a shade with contrast of 4.5:1 or higher against white.
<span
className={`text-sm ${bitwardenOrganizationAuthToken.valid ? "text-green-600" : "text-red-600"}`}
><span
className={`text-sm ${bitwardenOrganizationAuthToken.valid ? "text-green-700" : "text-red-600"}`}
>Typography
Spacing
Components
Craft
Working well
- Reusing `<ClearCredentialDialog>` for the destructive "Clear Credential" action instead of hand-rolling a confirm dialog is the right call: it keeps focus trapping and confirmation copy ("Clear Azure Client Secret Credential?") consistent with every other credential form in the app instead of inventing a one-off modal.
- The z-index override ships with a comment explaining exactly why this dialog needs to outrank the base layer (nested inside the cloud integrations tile dialog). That's the right instinct: a magic number alone is debt, but a magic number with a documented reason is at least traceable when someone revisits it.
- Feature-detecting `ResizeObserver` before using it (`if (typeof ResizeObserver === "undefined") return;`) is the right defensive pattern for a shared component: it keeps jsdom-based tests from crashing while still getting the real behavior in browsers, without needing a separate test-only code path.
- The 'Update Credential' button swaps its own label to "Updating..." while `isUpdating` is true instead of showing a separate spinner element. This keeps the feedback anchored exactly where the user's eye already is, so they don't have to look elsewhere on the page to know the click registered.
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.