Skip to content

UI/UX audit: reduced-motion, keyboard parity, missing aria-labels, empty states #220

@stultus

Description

@stultus

Context. Scriptty is a Tauri desktop app (macOS / Windows / Linux). This audit is desktop-only — recommendations that the source skill carried over from iOS/Android (44pt touch targets, swipe gestures, system gesture conflicts, tap delay, mobile haptics, safe-area insets, RN-specific guidance) have been filtered out. What's left is desktop-applicable: keyboard parity, screen-reader naming, focus management, reduced-motion, hover-discoverability, and empty / error states. Click target minimum used here is 32×32 visual (mouse precision); a 44px hit area via padding is fine but not required.

What

Comprehensive UI/UX audit of every Scriptty surface (Welcome, Editor, TitleBar, StatusBar, Scene Navigator, Scene Cards, Story Mode, Episode Cards, Outline Peek, all modals, both toasts, FormatBubble, Find/Replace bar, Command Palette). Aesthetic is locked — this issue is purely interaction quality, accessibility, and onboarding.

The editorial-vocabulary chrome is well-applied across surfaces; what's missing is a layer of accessibility scaffolding (reduced-motion, aria-labelledby on modals/toasts, drag keyboard alternatives, aria-pressed/current parity) and a few onboarding gaps (empty states on Cards / Episodes / Stats, recent-file path validation).

Findings below are grouped by severity, with file references. Use the checkboxes to track progress; sub-issues can split a row off if it's heavier than expected.


⛔ Must fix

Either blocks accessibility (keyboard / screen-reader users can't complete a flow) or actively misleads new writers.

  • Global prefers-reduced-motion support is missing. Every keyframe / transition (modal-in, backdrop-fade, dp-in, bubble-in, import-slide-in, update-slide-in, all panel slide / chevron / hover transitions across TitleBar / StatusBar / FormatBubble / OutlinePeek / LeftPanel / SceneNavigator / SeriesEpisodeList / DatePicker / Editor) declares its own duration without honouring the media query. Add a single rule to src/routes/+layout.svelte (@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; animation-iteration-count: 1 !important; } }) plus reset --motion-fast/base/slow tokens to 0ms so all timed work snaps. Verified missing in src/routes/+layout.svelte, MetadataModal.svelte, UpdateToast.svelte:177, ImportSummaryToast.svelte, FormatBubble.svelte:233, OutlinePeek.svelte:184, LeftPanel.svelte:34, SceneNavigator.svelte:895, SeriesEpisodeList.svelte:528/598/624/791, DatePicker.svelte:592–619, Editor.svelte:799–804, TitleBar.svelte:789–797.
  • Scene reorder has no keyboard path. SceneNavigator.svelte:614 (six-dot drag handle) is mouse-only. Keyboard users can't reorder scenes at all. Add Alt+ArrowUp / Alt+ArrowDown (or a "Move up / Move down" affordance that appears on item focus) and announce the move via aria-live. Same fix needed for SeriesEpisodeList.svelte:278.
  • Episode list lacks keyboard navigation. SeriesEpisodeList.svelte doesn't implement the ArrowUp/Down/Home/End roving-tabindex pattern that SceneNavigator.svelte:108–126 already has. Lift the helper or duplicate the keymap so series writers get parity with film writers.
  • ImportSummaryToast + UpdateToast + HelpModal + AboutModal + MetadataModal (header) are missing aria-labelledby. aria-modal="true" is set but the title isn't linked, so VoiceOver / NVDA reads "dialog" with no name. Add aria-labelledby="<title-id>" on the modal-backdrop element + matching id on the .mh-title. Same one-line fix applies to both toasts: link the role-status container to the title span. Refs: HelpModal.svelte:116, AboutModal.svelte:48, ImportSummaryToast.svelte:93, UpdateToast.svelte:25.
  • FindReplaceBar is unlabeled for screen readers. FindReplaceBar.svelte:151–157 — Find/Replace inputs use only placeholder= (no aria-label); the SVG prev/next buttons (174–197) have title but no aria-label. Add aria-label="Find in document" / "Replace with" to inputs and aria-label="Previous match (Shift+Enter)" / "Next match (Enter)" to the icon buttons.
  • SceneCardsView empty state is empty. SceneCardsView.svelte:972–1255 — a fresh screenplay shows a bare grid with no guidance. Add a centered empty card explaining how scene cards relate to scene headings ("Scene cards are auto-created from scene headings in the editor — switch to Writing and add INT. ROOM — DAY to populate this grid").
  • EpisodeCardsView rename / delete buttons disappear at rest. EpisodeCardsView.svelte:711–722.ep-actions { opacity: 0.55 } at rest, full only on hover. Keyboard-only users see no affordance and the resting state misleads even mouse users. Pin to opacity: 1 (or 0.85) at rest; brighten further on hover.
  • Close buttons fall below the 32×32 desktop minimum across modals. Quick sweep needed:
    • MetadataModal.svelte:502–517 — 30×30
    • SettingsModal.svelte:438–459 — 26×26
    • ImportWizardModal.svelte:84–86 — 24×24
    • NewProjectDialog.svelte:202–206 — 28×28
    • PasteScriptDialog.svelte:218–239 — 28×28
    • FindReplaceBar.svelte:304–325 .bar-btn — 26×26
      Bump to 32×32 (visual) with 44×44 hit area via padding so click latency stops mattering.

⚠️ Should fix

Real UX gaps; the writer can complete the flow but the surface trips them.

  • StatusBar save-state dot conveys meaning by colour alone. StatusBar.svelte:155, 330–340 — amber pip vs green dot. CVD users can't distinguish "saved" from "unsaved". Add a tiny inline glyph (✓ / ●) or a text label, or rely on the existing dirty-pip in TitleBar plus an aria-label on the StatusBar dot.
  • Dirty pip and incomplete-meta pip have no accessible name. TitleBar.svelte:219 (dirty), TitleBar.svelte:397 (meta-incomplete). Wrap both with <span aria-label="…" title="…"> so the screen reader and the hover tooltip both speak the meaning.
  • LeftPanel doesn't move focus inward when it opens. LeftPanel.svelte — keyboard users press Cmd+\, the panel slides open, focus stays on the toggle button. Move focus to the active scene/episode item on open so subsequent ArrowUp/Down works without a Tab.
  • StoryModeView tabs aren't a real ARIA tablist. StoryModeView.svelte:189–204 — Left/Right arrow keys don't cycle. Add the standard tablist keymap (role="tablist" is on the parent; just wire the arrow handler).
  • SeriesEpisodeList non-active episode subtree is muted but still tab-focusable. SeriesEpisodeList.svelte:388.non-active { opacity: 0.78 } reads as "this is somewhere else", but Tab still lands inside. Either suppress focus there with inert (Svelte 5: {@attach inert} or tabindex="-1" on every descendant), or add a clear "Switch to this episode first" message when focused.
  • Drag-handle drag state is invisible to AT. SceneNavigator.svelte:557–559 and SeriesEpisodeList drag — no aria-grabbed / aria-dropeffect / aria-live announcement. When the keyboard reorder lands (must-fix above), make sure the live region narrates "Scene 5 moved to position 3".
  • Welcome recent-file list breaks silently when the path is gone. WelcomeScreen.svelte:156–170 — clicking a deleted/moved .screenplay should remove it from the list and surface a toast ("File moved or deleted — removed from recents"). Today the open-flow probably surfaces a backend error string.
  • StoryModeView ↔ Writing view loses cursor position. Cross-view switch (+page.svelte view-switcher) doesn't restore the editor's selection or scroll offset on return. Stash editorStore.view.state.selection per-view and restore on re-entry.
  • StatisticsModal sort preference resets on every open. StatisticsModal.svelte:204–229 — if intentional, surface "Sort preference resets when reopened" in the column-header tooltip; otherwise persist to component-local state (or localStorage).
  • EpisodeCardsView status pill (Outline/Draft/Revision/Final) doesn't read as interactive. Lines 370–381 — clicking cycles, but cursor stays default and there's no hover treatment. Add cursor: pointer + a subtle box-shadow or background change on hover; tooltip "Click to advance: Outline → Draft → Revision → Final".
  • SettingsModal custom dropdown lacks the listbox-pattern wiring. SettingsModal.svelte:114–166role="listbox" is on the wrapper but the trigger button doesn't carry aria-controls / aria-expanded / aria-haspopup="listbox", and individual options aren't role="option" with aria-selected. Either flesh out the pattern or fall back to a native <select> with custom styling.
  • SceneCardsView "Group by location" silently disables drag-reorder. Lines 1022–1034 — when grouping is on, the gutter handle is disabled; the title attribute changes but visual state doesn't. Add .card-gutter[disabled] { opacity: 0.4; cursor: not-allowed; }.
  • ExportModal async buttons need a verified aria-busy state. Lines 70–72, 1120–1232 — confirm Plain text / Fountain / PDF buttons all show the spinner and set aria-busy="true" and aria-disabled="true" during in-flight; cancel-during-export should be impossible.
  • StatusBar lacks a landmark role. StatusBar.svelte:171 — add role="contentinfo" so AT users can jump there with the landmark shortcut.
  • +page.svelte editor area lacks role="main" / <main> semantic. Already wrapped in <main> (line 1004); just confirm there's no nested role="main" and that the OutlinePeek + StatusBar live outside it.
  • CTA hierarchy on Welcome muddles "New Film" vs "New Series". WelcomeScreen.svelte:95–104 — both look equally weighted to a first-time visitor. Add a one-line explainer below each ("One screenplay" / "Multiple episodes in one file").

✨ Nice to have

Polish and discoverability — improves perceived quality but no one is blocked.

  • Cmd+1 / Cmd+2 / Cmd+3 direct-select for Writing / Cards / Story. +page.svelte:610–702 already has the View menu listeners; add the keyboard shortcuts so power users skip the toggle commands.
  • StoryModeView word-count is only in StatusBar. Add a muted in-view badge under the section title ("№ 02 · Synopsis · 427 words") so the writer doesn't need to glance away.
  • StatisticsModal empty-doc state. StatisticsModal.svelte:42–72 — when stats.sceneCount === 0, the Overview tab is a wall of zeros. Show "Write a scene to populate statistics" with a "Switch to Writing" button.
  • Editor first-launch hint. Editor.svelte:491 — when the editor mounts on a brand-new doc with one empty scene_heading, render a placeholder cue ("Start with a scene heading like INT. ROOM — DAY"). Disappears on first keystroke.
  • WelcomeScreen recent-files keyboard nav. WelcomeScreen.svelte:156–170 — Arrow Up/Down to move focus, Enter to open. Currently Tab-only.
  • EpisodeCardsView inline rename has no commit feedback. EpisodeCardsView.svelte:265–282 — show a brief check-mark or accent flash when blur commits the rename so the writer knows it landed.
  • EpisodeCardsView scene-peek "+ N more scenes" is a label, not a button. EpisodeCardsView.svelte:346–365 — make it a button (or change copy to "Open episode to see all scenes") so the drill-down path is explicit.
  • OutlinePeek segment timeline uses role="list" with non-list children. OutlinePeek.svelte:106 — switch to role="presentation" (or migrate to role="tablist" since segments behave like tabs).
  • OutlinePeek may scale poorly past ~200 scenes. OutlinePeek.svelte:106 — virtualise segments outside the viewport; today the entire timeline is in the DOM.
  • TitleBar view tabs lack aria-current. TitleBar.svelte view tabs — add aria-current="page" (or "true") on the active tab so AT users know which view is up.
  • Episode popover active episode lacks aria-current. TitleBar.svelte:188–216.ep-pop-row.active should carry aria-current="true".
  • CommandPalette listbox is unnamed. CommandPalette.svelte:158aria-label="Filtered commands" on the role=listbox container.
  • Standardise motion durations on the design tokens. +layout.svelte:243–246 defines --motion-fast/base/slow but most components hardcode 120ms/160ms/200ms literals. Sweep + replace; this also makes the reduced-motion override (must-fix Add build and release badge #1) a single token override.
  • SceneCardsView "Compact view" toggle has low discoverability. SceneCardsView.svelte:940–960 — first-mount tooltip ("Toggle between roomy cards and a one-line-per-scene overview") would help.
  • PasteScriptDialog placeholder text wraps awkwardly under ~720px. PasteScriptDialog.svelte:104–119 — placeholder is multi-line; below the modal's max-width: 92vw it can collide. Consider truncation or a help-text block above the textarea.
  • Story-mode placeholder voice is inconsistent. StoryModeView.svelte:45–64 — mix of "The core premise. One to three lines." vs "Full narrative prose". Standardise to the editorial-masthead voice used elsewhere.
  • NewProjectDialog title input is bottom-border-only. NewProjectDialog.svelte:256–282 — on the cream paper field in light mode the border is hard to spot; add a subtle focus background. Also missing aria-required="true".
  • ImportSummaryToast middle-dot count separator reads as "dot" to screen readers. ImportSummaryToast.svelte:178–189{counts.join(' · ')} — wrap each count in its own <span> so the SR hears them as a list, or override the aria-label to a comma-joined version.
  • SceneCardsView DatePicker is hidden inside a disclosure. SceneCardsView.svelte:1197–1202 — add a hint in the production-summary text so writers know clicking expands a calendar picker.

Out of scope (this issue)

  • Aesthetic / vocabulary changes — locked per CLAUDE.md.
  • New features (autosave indicator beyond dirty-pip, undo for destructive actions, draft history) — separate issues.
  • RTL support — DatePicker.svelte:121 flagged the future-proofing concern but RTL isn't a v1 goal.

Acceptance

This issue is done when:

  1. Every must-fix box is ticked (or explicitly closed with a sub-issue and rationale).
  2. A reduced-motion smoke test passes (set OS-level reduce-motion, open every modal + drawer + view-switch + import flow → no animations should run).
  3. A keyboard-only smoke test passes the New Film → write a scene → open Cards → reorder a scene → open Stats → close → quit flow without the mouse.
  4. VoiceOver smoke test announces every modal title on open and every dirty/incomplete pip on focus.

Total: 9 must-fix · 14 should-fix · 18 nice-to-have across 23 surfaces.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestui/uxUI/UX or design issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions