Skip to content

Give the app a focus state, a brand selection, and honest reduced motion - #52

Merged
milind-soni merged 3 commits into
milind-soni:mainfrom
chidhu07:design-foundations
Aug 18, 2026
Merged

Give the app a focus state, a brand selection, and honest reduced motion#52
milind-soni merged 3 commits into
milind-soni:mainfrom
chidhu07:design-foundations

Conversation

@chidhu07

@chidhu07 chidhu07 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

The app has no focus state

outline-none appears in nine components. Nothing anywhere puts a ring back — there is not a single focus-visible: utility in src/. I ran the app and focused every text field in it:

field ring drawn on focus
composer (Message …) nothing
sidebar search nothing
answer box on an approval card nothing
onboarding name nothing
onboarding email nothing
Composio Connect key nothing
Composio API key nothing
Box token nothing

Eight of eight. A keyboard user pasting keys into App Settings cannot tell which of the three fields they are typing into.

In both shots below, the lower field (ak_…) is focused — in before, the caret is the only thing that says so, and it is indistinguishable from the unfocused field above it.

before

after

Buttons do keep the browser default ring, so this is not "the app is unusable by keyboard" — it is that a hand-tuned dark UI falls back to Chrome's stock halo on buttons and to nothing at all on inputs.

The composer, focused, before and after:

How it lands without touching those nine files

Every one of the nine components has an open PR against it — Composer.tsx alone is in seven. So none of them are edited here.

The rules are unlayered. Tailwind ships its utilities inside @layer utilities, and unlayered styles outrank every layer regardless of specificity, so a plain :focus-visible rule in styles.css beats outline-none with no !important and no component change. Verified in the running app:

after mouse click on <BUTTON>: outline none 3px          ← click still paints nothing
after Tab           on <INPUT>: outline solid 2px rgb(69, 159, 254)

That is :focus-visible doing its job: pointer users never see a ring, keyboard users always do.

The generic ring sets no border-radius on purpose — an outline already follows the element's own corners, and 26 controls in src/ are rounded-full, which a fixed radius would square off. Text fields are the one exception: most already carry a radius, but the composer input has none, so its ring came out as a hard rectangle sitting inside a pill.

Three smaller gaps in the same file

::selection was never styled. Dragging across a bot's reply painted Chrome's default blue straight through a palette that was pixel-sampled off the real app. It now tints --color-accent.

prefers-reduced-motion only ever reached the mascot. Panels still flew in, cards still popped, every hover transition still ran. Measured, before and after:

no-preference reduce (before) reduce (after)
animate-panel-in 0.24s 0.24s 0.01ms
animate-pop-in 0.2s 0.2s 0.01ms
a 300ms transition 0.3s 0.3s 0.01ms

Durations collapse rather than animation: none, because a spinner frozen mid-turn reads as a hung app. .maus-motion keeps its stronger existing rule — it has higher specificity, so the mascot still stops dead rather than ticking.

The scrollbar thumb was inert — no hover state. It has one now, plus a corner rule for the horizontal track.

Scope

One file, +70 lines, 0 deletions, no component changes.

No palette value changed, no radius token, no type. This does not restyle anything that was already designed — it fills in the states that had no styling at all.

I checked this against all 26 open PRs by exact path: src/styles.css is touched by none of them, so this should merge clean regardless of what lands first.

  • pnpm typecheck clean
  • pnpm test 52 passed / 33 skipped, unchanged from main
  • pnpm exec vite build succeeds; CSS bundle 40.43 kB → 41.11 kB, gzipped 8.61 kB → 8.77 kB (+0.15 kB)

Verified on

Windows 11 / Chrome 141, driving the real app against a throwaway ~/.openmausbot. Not verified on macOS — I do not have a machine, and the focus ring is plain CSS so I would not expect divergence, but the composer ring in particular is worth a glance from someone on the packaged app.

Summary by CodeRabbit

  • Accessibility

    • Added clearer keyboard focus indicators across interactive elements.
    • Improved focus styling for text fields.
    • Expanded reduced-motion support to minimize animations, transitions, delays, and smooth scrolling.
  • Style

    • Added custom text selection colors.
    • Improved scrollbar sizing, hover feedback, and corner styling.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@milind-soni, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Limit details: You’ve used all 3 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f75d4a93-ba83-4a38-b117-ce4ebf603bf3

📥 Commits

Reviewing files that changed from the base of the PR and between 1e458b1 and b35023b.

📒 Files selected for processing (1)
  • src/styles.css

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fb3f7d4d-1922-4f5b-aa70-83f8387126dc

📥 Commits

Reviewing files that changed from the base of the PR and between 47fbc1c and e9880b7.

📒 Files selected for processing (1)
  • src/styles.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/styles.css

📝 Walkthrough

Walkthrough

The stylesheet adds focus indicators, custom selection colors, scrollbar styling, and reduced-motion overrides. It also defines a dedicated focus color and disables smooth scrolling for reduced-motion users.

Changes

Accessibility and interaction styling

Layer / File(s) Summary
Focus, selection, and scrollbar styling
src/styles.css
Adds a focus color, keyboard-only focus outlines, inset form-control focus styles, custom selection colors, scrollbar sizing, hover feedback, and transparent corners.
Reduced-motion behavior
src/styles.css
Minimizes animation and transition timing and changes scrolling to immediate behavior when reduced motion is enabled.

Estimated code review effort: 2 (Simple) | ~10 minutes

Mergeability Score: ⚪ Minimal · up to e9880

This localized stylesheet change adds focus visibility, selection styling, reduced-motion handling, and scrollbar states without changing component behavior; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main focus, selection, and reduced-motion changes, although it omits scrollbar styling.
Description check ✅ Passed The description covers the motivation, changes, screenshots, verification, scope, and platform coverage, although it omits the template checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@chidhu07

Copy link
Copy Markdown
Contributor Author

Replaced the screenshots — the first set was bad evidence, and the before/after read as identical. Thanks for the nudge.

My capture harness froze CSS animations with animation-delay: -0.01s to stop the mascot mid-loop. That also pinned animate-panel-in (opacity 0 → 1) near frame zero, so App Settings got photographed mid fade-in and the whole plate came out dimmed, ring included. Freezing at the end frame instead (-10s) captures the panel at full opacity.

The CSS in this PR did not change. Only the photograph did: max per-pixel delta between before and after went from 75 to 407, which is what #459ffe over a dark field should measure. The new crops are also tight on the field, so a 2px ring survives being scaled down inside a PR body.

… motion

Four foundations the design system was missing. All of it lives in
src/styles.css with no component changes, so it composes with everything
currently in flight.

FOCUS. Nine components clear the UA outline with `outline-none`, and
nothing anywhere put a ring back — there is not one `focus-visible:`
utility in the app. Measured in the running app, all eight text fields
focused invisibly: the composer, sidebar search, the answer box on an
approval card, both onboarding fields, and all three API key fields. A
keyboard user filling in App Settings could not tell which key they were
typing into. Buttons kept the browser default, which is legible but reads
as an unstyled page inside a hand-tuned dark UI.

The rules are deliberately unlayered. Tailwind ships utilities in
@layer utilities and unlayered styles outrank every layer, so this beats
`outline-none` with no !important and without editing the nine
components — every one of which has an open PR against it. Verified: a
mouse click still paints nothing, Tab paints a 2px accent ring.

The generic ring sets no radius, because an outline already follows the
element's own corners and 26 controls here are `rounded-full`. Text
fields are the exception: most carry a radius already, but the composer
input has none, so its ring came out a hard rectangle inside a pill.

SELECTION. ::selection was never styled, so dragging over a bot's reply
painted Chrome's default blue through a palette that was pixel-sampled
off the real app. It now tints the accent.

REDUCED MOTION. The mascot was the only thing honouring
prefers-reduced-motion. Panels still flew in, cards still popped, and
every hover transition still ran. Now the app stills: panel-in 0.24s and
pop-in 0.2s both collapse to 0.01ms under reduce, and normal playback is
untouched without it. Durations collapse rather than `animation: none`,
because a spinner frozen mid-turn reads as a hung app.

The scrollbar thumb also gets a hover state; it was inert.

No palette values changed, no radii tokens, no type. Nothing here
restyles what was already designed — it fills in what had no styling.
@chidhu07
chidhu07 force-pushed the design-foundations branch from 47fbc1c to e9880b7 Compare August 13, 2026 12:36
@chidhu07

Copy link
Copy Markdown
Contributor Author

Rebased onto ef08733 — this was branched before the Chat UI overhaul and the #17 Windows merge landed, so it was 14 commits behind.

Rebase was clean and the diff is unchanged at +70/−0 in src/styles.css. Re-checked against current main rather than assuming:

  • styles.css on main gained 29 lines in the overhaul, but still has 0 focus-visible, 0 ::selection, 0 outline: and 0 scrollbar-thumb:hover — so nothing here is now redundant
  • re-probed the running app on the rebased branch: all 8 text fields still take the ring
  • pnpm typecheck clean, pnpm test 59 passed / 35 skipped, vite build succeeds

@milind-soni milind-soni left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The focus, selection, and scrollbar work looks good. The broad reduced-motion rule needs one correction before merge: setting animation-duration: 0.01ms on infinite animations leaves them infinite and makes bounce/spin animations advance thousands of cycles per frame, producing jitter/flicker rather than reduced motion. Please cap iteration count for motion animations or give continuous progress indicators an explicit reduced-motion state instead of globally accelerating them.

@milind-soni

Copy link
Copy Markdown
Owner

Reviewed against current main, and the focus work was measured in the real runtime (Electron 43 / Chromium 150) rather than read.

The blocker is that the premise doesn't hold: Chromium always matches :focus-visible on text inputs, textareas and <select> regardless of how they were focused. So "pointer users never see a ring" isn't true — clicking the composer, the API-key fields, the sidebar search or the group <select> all gain a 2px ring after this change, and the <select> also loses its pill radius (rounded to 10px). Buttons behave as described; the text widgets don't.

That's fixable — scope the ring so it doesn't fire for pointer focus on text widgets, or accept the ring deliberately and keep the existing radii — but as written it restyles focus for every mouse user, which isn't what the PR says it does. The reduced-motion half is sound.

Happy to re-review if you rework it; a quick way to check is to click each of those four widgets in the packaged app and compare computed outline/border-radius before and after.

@milind-soni milind-soni left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed on current main. Text controls now keep their existing radii and intentionally show an inset focus ring for both pointer and keyboard focus. Reduced-motion mode stops spatial animation and uses a slow opacity pulse for loading indicators instead of accelerating infinite animations. Full local tests, production build, packaged-server smoke, and Electron syntax checks pass.

@milind-soni
milind-soni merged commit b52a42a into milind-soni:main Aug 18, 2026
1 check passed
@chidhu07

Copy link
Copy Markdown
Contributor Author

Both corrections were right, and thanks for fixing them in place rather than bouncing the PR.

Reduced motion. You were right that this was concrete, not theoretical — at the reviewed commit there were 16 infinite animations outside .maus-motion: 12 animate-spin, 3 animate-bounce (the typing dots in ChatView), 1 animate-pulse, plus caret-blink and shimmer in the stylesheet. animation-duration: 0.01ms would have driven every one of those at roughly a cycle per frame — strobing, aimed squarely at the users the media query exists to protect. My own probe missed it because I tested three planted <div>s that had no infinite animation on them, so the measurement could not see the failure mode that mattered. The reduced-loader-pulse fallback is the right answer to the "frozen spinner reads as hung" worry I was trying to solve by accelerating.

Focus. Also right, and my evidence should have caught it: my "mouse click paints nothing" check clicked a <button> and I generalised that to text controls, while every other probe in the PR focused inputs programmatically and saw the ring — which is Chromium matching :focus-visible on text widgets exactly as you describe. Switching those to :focus and stating it as deliberate is more honest than what I wrote.

The border-radius removal is a straight bug fix on my part. My comment claimed "text fields are never the rounded-full controls" — I never checked <select>, and asserted it as fact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants