skyvern-ai on GitHub

skyvern-ai/skyvern

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

30 files reviewed·July 25, 2026

View on GitHub

Low

Design risk in this codebase.

7issues
Serious · top 6 shown below

Top fix

Add error handling and user feedback to async copy and mint actions

See the fix

Verdict

Solid component plumbing keeps failing to tell users when something actually failed. Copy buttons lie with success toasts, webhook tests invite double-clicks, and one missing min-w-0 lets a workflow ID overflow its box.

Files Rams reviewed

skyvern-frontend/src/components/PageLayout.tsx

skyvern-frontend/src/components/AgentFilterDropdown.tsx

skyvern-frontend/src/components/AnimatedWave.tsx

skyvern-frontend/src/components/ApiWebhookActionsMenu.tsx

skyvern-frontend/src/components/ArtifactDownloadLink.tsx

skyvern-frontend/src/components/ArtifactVideo.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/GoogleOAuthClientConfigForm.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

94/100

UX

3 serious
UXSerious

skyvern-frontend/src/components/ArtifactDownloadLink.tsx:49

A failed URL mint leaves an open tab or click with zero feedback

handleClick opens a blank tab synchronously, then calls void freshArtifactUrl(credentialGetter, href).then((url) => { newTab.location.href = url } or window.location.assign(url) }). There is no .catch, so if freshArtifactUrl rejects, the opened tab stays permanently blank (or the click does nothing in the same-tab case) with no error surfaced.

Why it matters

A user clicking a download link that fails to mint sees an empty tab or an inert link and has no way to know whether to retry or report a problem.

Fix

Attach a .catch to async navigation promises and surface a visible error state instead of leaving the tab blank.

void freshArtifactUrl(credentialGetter, href).then((url) => {
  if (newTab) {
    newTab.location.href = url;
  } else {
    window.location.assign(url);
  }
});
void freshArtifactUrl(credentialGetter, href)
  .then((url) => {
    if (newTab) {
      newTab.location.href = url;
    } else {
      window.location.assign(url);
    }
  })
  .catch(() => {
    if (newTab) {
      newTab.close();
    }
    toast({
      variant: "destructive",
      title: "Download Failed",
      description: "Could not open this artifact. Try again.",
    });
  });
UXSerious

skyvern-frontend/src/components/ApiWebhookActionsMenu.tsx:71

Copy cURL and PowerShell items show a success toast even when the copy fails

Both "Copy cURL (Unix/Linux/macOS)" and "Copy PowerShell (Windows)" call copyText(...).then(() => toast({variant: "success", ...})) with no .catch. If the clipboard write rejects (permissions, insecure context, browser quirk), no toast fires at all and the user has no signal the copy didn't happen.

Why it matters

A user who believes the command copied will paste nothing into their terminal, then debug a mystery empty paste instead of retrying the copy immediately.

Fix

Attach a .catch to clipboard promises and show an error toast so failures are as visible as successes.

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: "Could not copy the cURL command to your clipboard.",
    });
  });
UXSerious

skyvern-frontend/src/components/ApiWebhookActionsMenu.tsx:103

"Test Webhook" gives no in-flight feedback, inviting repeat triggers

The "Test Webhook" DropdownMenuItem runs setTimeout(() => onTestWebhook(), 0) on select, but the item never disables or shows a pending state while the webhook call is running. Only webhookDisabled (an external prop) can disable it.

Why it matters

A user unsure whether the click registered can reopen the menu and fire Test Webhook multiple times in a row, triggering duplicate webhook deliveries with no visual cue anything happened.

Fix

Disable the trigger and show a pending state for the duration of an in-flight async action.

<DropdownMenuItem
  disabled={webhookDisabled}
  onSelect={() => {
    setTimeout(() => onTestWebhook(), 0);
  }}
>
  Test Webhook
</DropdownMenuItem>
<DropdownMenuItem
  disabled={webhookDisabled || isTestingWebhook}
  onSelect={() => {
    setIsTestingWebhook(true);
    setTimeout(() => {
      Promise.resolve(onTestWebhook()).finally(() =>
        setIsTestingWebhook(false),
      );
    }, 0);
  }}
>
  {isTestingWebhook ? "Testing…" : "Test Webhook"}
</DropdownMenuItem>
98/100

Accessibility

1 serious
AccessibilitySerious

skyvern-frontend/src/components/AgentFilterDropdown.tsx:122

"Clear all" button renders under the 44px minimum touch target

The "Clear all" button uses className="h-7 px-2 text-xs", rendering at 28px tall inside a py-1 container. That's well under the 44px minimum interactive target recommended by WCAG 2.5.5.

Why it matters

On mobile, a 28px target is easy to miss or mis-tap, especially next to the search input directly above it, causing accidental dismiss or repeated failed taps.

Fix

Size interactive controls to at least 44x44px, or pad the hit area even if the visual chip stays small.

<div className="flex justify-end border-b px-2 py-1">
  <Button
    type="button"
    variant="ghost"
    size="sm"
    className="h-7 px-2 text-xs"
    onClick={() => onChange([])}
  >
    Clear all
  </Button>
</div>
<div className="flex justify-end border-b px-2 py-2">
  <Button
    type="button"
    variant="ghost"
    size="sm"
    className="h-9 px-3 text-xs"
    onClick={() => onChange([])}
  >
    Clear all
  </Button>
</div>

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 free
98/100

Typography

1 serious
TypographySerious

skyvern-frontend/src/components/AgentFilterDropdown.tsx:166

Truncated workflow ID overflows because the flex child has no min-w-0

In the "Selected" CommandGroup, the workflowPermanentId span uses className="truncate font-mono text-xs text-muted-foreground" as a direct child of a flex CommandItem (className="gap-2"). Flex items default to min-width: auto, so a truncated child can't shrink below its content width and the ellipsis never triggers, letting long IDs spill past the row edge.

Why it matters

Long workflow IDs break the dropdown's fixed-width layout, pushing or clipping adjacent rows and making the selected-agents list look broken instead of a clean single-line list.

Fix

Add min-w-0 to any flex child that needs its truncate class to actually clamp width.

<CommandItem
  key={workflowPermanentId}
  value={workflowPermanentId}
  className="gap-2"
  onSelect={() => toggleWorkflow(workflowPermanentId)}
>
  <Checkbox
    checked
    tabIndex={-1}
    className="pointer-events-none"
  />
  <span className="truncate font-mono text-xs text-muted-foreground">
    {workflowPermanentId}
  </span>
</CommandItem>
<CommandItem
  key={workflowPermanentId}
  value={workflowPermanentId}
  className="gap-2"
  onSelect={() => toggleWorkflow(workflowPermanentId)}
>
  <Checkbox
    checked
    tabIndex={-1}
    className="pointer-events-none"
  />
  <span className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground">
    {workflowPermanentId}
  </span>
</CommandItem>
98/100

Motion

1 serious
MotionSerious

skyvern-frontend/src/components/AnimatedWave.tsx:20

Infinite per-character wave animation ignores prefers-reduced-motion

Each character span gets className="animate-wave inline-block" with animationIterationCount: "infinite" and no media query guard in the injected <style> block. The wave keeps running for every visitor regardless of their OS motion setting.

Why it matters

Vestibular-sensitive users who have set reduced motion at the OS level still get a continuous bobbing animation with no way to opt out short of leaving the page.

Fix

Wrap infinite or repeating animations in a prefers-reduced-motion: no-preference guard, or disable animation-iteration-count under prefers-reduced-motion: reduce.

  .animate-wave {
    animation-name: wave;
  }
`}</style>
  .animate-wave {
    animation-name: wave;
  }
  @media (prefers-reduced-motion: reduce) {
    .animate-wave {
      animation: none;
    }
  }
`}</style>

Color

No issues found

Spacing

No issues found

Components

No issues found

Craft

No issues found

Working well

  • ArtifactDownloadLink opens the new tab synchronously inside the click handler before the async URL mint resolves, then assigns location.href once ready. That ordering is the correct fix for popup blockers in Safari/Firefox while preserving new-tab behavior.
  • AgentFilterDropdown debounces search input with a separate isTyping flag to drive the skeleton loading state, which avoids skeleton flicker between fast keystrokes and real fetches: a subtle but correct pattern for async filter UIs.
  • AnimatedWave staggers each character's animation-delay by index * 0.1s, producing a per-letter ripple without extra markup or JS timers: a clean, readable way to sequence character-level motion.

Scored July 25, 2026 with Rams Engine v0.0.3 · Engine changelog
First scored July 9, 2026: 59/100. This rescore on v0.0.3: 86/100.

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