Skip to content

fix: design-system audit — motion, interaction states, focus & accessibility#856

Open
ravisuhag wants to merge 42 commits into
mainfrom
design-audit-clean
Open

fix: design-system audit — motion, interaction states, focus & accessibility#856
ravisuhag wants to merge 42 commits into
mainfrom
design-audit-clean

Conversation

@ravisuhag

@ravisuhag ravisuhag commented Jul 12, 2026

Copy link
Copy Markdown
Member

Summary

A design-system-wide audit of the component library, consolidating the animation work and the broader interaction/craft pass into a single branch.

What's covered

  • Motion — shared easing/duration/scale tokens; interruptible, origin-anchored entrances for overlays (popover, menu, tooltip, select, combobox, preview-card); indeterminate Progress; drawer 1:1 swipe tracking; reduced-motion fallbacks throughout.
  • Interaction states — hover / press / focus feedback for buttons, toggles, chips, and the text-field family (input, field, number-field, text-area, otp-field), with a unified invalid state.
  • Focus — a consistent, tokenized :focus-visible ring across the library.
  • Accessibility — reduced-motion and reduced-transparency fallbacks; a calendar day can no longer be selected by keyboard while the grid is loading; announcement-bar action-button states.
  • Data views — selected / pressed row states, sort-icon direction, sticky-header hairlines that appear only once content scrolls under them, tabular figures.
  • Media — image and avatar fade-in with a cached-image skip and a one-shot fallback guard.
  • Tokens & typography — motion/interaction tokens consolidated into styles/effects.css; Inter body tracking and the h1–h4 type ramp retuned.

Related issues

Closes #681 — consistent, tokenized :focus-visible ring applied across the library.
Closes #621 — Image error/loading handling: one-shot fallback guard plus load-state fade-in.
Closes #625 — Link's remaining items (focus-visible, reduced-motion) are now done.

Partially addresses:

Test plan

  • pnpm build passes.
  • Component test suites pass. A pre-existing @base-ui module-resolution failure affects a handful of unrelated test files on main as well.

ravisuhag added 30 commits July 13, 2026 02:58
Compose the shared easing, duration, pressed-scale and focus-ring tokens so
components stop hand-rolling their own timings and curves.
Step body letter-spacing and h1-h4 line-heights so they change monotonically
across sizes and follow Inter's own tracking curve.
Give numeric columns tabular figures so digits align, alongside the shared
row-selection and sticky-header styling that lives in the same two stylesheets.
A slightly faster spinner raises perceived load speed. Recomputed all
8 pole animation-delays to keep the same 1/8-cycle stagger ratio at
the new duration, so the sweep still rotates evenly.
Centering used translateX(-50%), which would collide with any
transform-based entrance. Move centering to inset+margin-inline:auto
(safe since the root is width:fit-content) so transform is free for
motion. Add a fade-in for everyone, plus an 8px rise from the
anchored edge under prefers-reduced-motion: no-preference. Exit stays
instant (no mount controller exists to hold the node past removal).
The error handler swapped src to the fallback with no re-entry guard --
if the fallback URL itself 404s, onError fires again and re-assigns
the same failing src, looping requests indefinitely. Track a one-shot
ref so the fallback is only ever assigned once; a second failure falls
through to showing the alt/broken rendering instead.

Both Image and Avatar's photo popped in abruptly with no load
transition. Image now tracks load state via a useLayoutEffect that
checks node.complete on mount/src-change -- already-decoded (cached or
SSR-painted) images stay visible with no fade; genuinely loading
images fade in over 150ms on load. Avatar's image only mounts once
Base UI's probe reports loaded, so its fade is a one-shot keyframe
animation, skipped entirely for cache-fast loads (<100ms between
loading and loaded) so a quick load isn't artificially delayed.
Reduced motion shows both instantly at full opacity, never invisible.
A display:block heading already fills its container in normal flow;
the explicit width:100% only mattered in flex/grid contexts, where it
made a heading claim the full row/track and push siblings around.
Ellipsis truncation is the only variant that needs an explicit width,
and it already declares its own -- removing the base rule loses
nothing there.

BREAKING (mild): consumers relying on the base width:100% inside flex/
grid parents (e.g. a centered heading, or a heading used as a full-row
flex child) will see it shrink to content width. One-line fix on their
side: set width:100% via className where the old behavior was
intentional.
The action button was the bar's only interactive element but had no hover,
press, or focus feedback. Add opacity-step hover/press states and a currentColor
focus ring that stays visible against the bar's coloured backgrounds.
…hiding it

Both variants hid the shared Tabs.Indicator and painted the active
state directly on the trigger, so switching tabs snapped instantly
instead of gliding like the default segmented variant. The indicator
already tracks the active tab via Base UI's --active-tab-* variables
for free -- hiding it threw that away.

Restyle the indicator per variant instead: standalone becomes the
active chip fill (background + border) gliding between tabs, with the
active trigger going transparent so the pill shows through (the same
trick the segmented variant already uses); plain becomes a 1px
underline sliding along the bottom edge, positioned via the trigger's
own --active-tab-height so it lines up with the reserved
border-bottom exactly. Both inherit the indicator's existing
--rs-duration-moderate/--rs-ease-in-out transition, gated behind
reduced motion, with no new media query needed.
Breadcrumb links changed colour instantly on hover while the rest of the library
eases. Add a short colour transition, gated behind prefers-reduced-motion, so the
hover matches every other interactive element.
Popover -- a larger floating surface than the tooltip -- cast a
lighter shadow (--rs-shadow-soft) than the tiny tooltip
(--rs-shadow-lifted), so elevation read backwards when both were on
screen. Match menu, a peer surface of similar size, which already uses
--rs-shadow-lifted.
overflow: scroll forced a permanently visible scrollbar gutter on
platforms with classic scrollbars, even when content didn't overflow.
Use auto. Also document via JSDoc that SidePanel intentionally has no
enter/exit animation -- it's persistent layout chrome like Sidebar,
not an overlay.
The scrollbar grew its whole track on hover, reflowing the page each time, and
the thumb never showed it was grabbed. Keep the track a constant, invisible
width so layout never shifts; draw the thumb thinner at rest and grow it to full
width on hover; and deepen its colour one step on hover and again while dragging.
The positioner transitioned top/left/right/bottom and the popup
transitioned width/height on every open -- but Base UI only writes
inset positioning and drives --popup-width/--popup-height when a
PreviewCard.Viewport child triggers its adaptiveOrigin mode. In the
common case (no Viewport), Base UI positions purely via transform, so
the four inset transitions were dead weight forcing unnecessary
transition-list overhead on every reposition.

Split into two rulesets: the default path transitions transform +
opacity only (compositor-only), while `:has(.viewport)` keeps the
inset/size transitions where they're the actual morph mechanism.
A sticky header drew its bottom hairline even at rest, before anything had
scrolled beneath it. Drive the hairline from a scroll-linked animation so it
fades in over the first bit of scroll, gated behind a real scroll container so
the plain table keeps its static separator.
Resting day cells used radius-2 while hover/selected/range-cap states
all used radius-5, so sweeping the pointer across the grid made each
cell's shape pop between rounded-square and pill. The radius-2 resting
state was never actually visible (the cell's background matches the
calendar root's), so unify on radius-5 -- zero visible states change,
one invisible one gets fixed. Hover/selection highlights stay instant,
per the audit's explicit rejection of transitions on direct-manipulation
surfaces.

Loading hard-swapped the whole table for a differently-sized Skeleton
in one frame, flashing on fast loads and jumping the popover height.
Keep the table mounted and overlay the skeleton, cross-fading via
opacity/visibility (~150ms, instant under reduced motion) so the grid
never disappears and the height never moves.
Dimming a link to 80% opacity on hover is a weak signal and makes the
text harder to read at the exact moment the user is targeting it. The
underline geometry now always exists (text-decoration-color:
transparent at rest) so hover is a pure color fade to currentColor --
zero layout shift, works for every Text color variant automatically.

Press gets a brief variant-agnostic dim (:active, opacity 0.7) instead
of a hardcoded color, since a fixed active color would clobber
non-accent variants. Timing moves from a hardcoded 0.2s to
--rs-duration-fast.
Proportional digits meant amounts jittered as values changed and
misaligned in table columns. Add a CSS module with
font-variant-numeric: tabular-nums as the default; className/style
still merge through as the opt-out escape hatch.
The floating copy button had no transition of its own duration (it
inherited the interactive tier's opacity fade), so its reveal moved at
hover-feedback speed instead of a dedicated reveal timing. Re-compose
the IconButton base transition and override just the opacity entry
with --rs-duration-fast/--rs-ease-out. The :focus-visible reveal rule
(added by the earlier focus-indicators pass) is merged into the same
selector as the hover reveal for clarity.
Row selection was wired up in data-table and data-view-beta (both set
data-state="selected") but no CSS anywhere styled it -- selecting a
row changed nothing on screen. Add an accent-subtle background for
selected rows (wins over hover/pressed) and a one-step-darker pressed
background for clickable rows, instant (no transition, matching the
house rule for direct-manipulation highlights like menu items).

Virtualized cells painted an opaque background that covered the row's
color entirely -- add `.virtualRow .virtualCell { background: inherit }`
so selection/hover/press show through in virtualized mode, same as the
non-virtualized cells already do.

data-view (no selection support) gets the same pressed-row treatment.

The bare Table primitive had no row-state hooks at all. Table.Row now
accepts an `interactive` prop (cursor + hover + pressed) and responds
to a consumer-set data-state="selected", with cells inheriting the row
background through the opaque .cell rule.
…bled cursor

Hovering an off toggle looked identical to the on state -- the control
lied about whether it was selected. Give hover a lighter tint and
pressed (Base UI's toggled-on state) a deeper, inset-shadowed look in
both standalone and group mode; hovering an already-on toggle now
deepens it one more step instead of reverting to the hover tint.

The content background and inset shadow had no transition at all
(only color did), so state changes snapped. Add a fast-tier transition
for both.

pointer-events: none on disabled toggles suppressed the declared
cursor: not-allowed, so disabled toggles gave zero affordance. Restore
pointer-events so the cursor shows, with explicit hover neutralizers so
a disabled-but-off toggle doesn't pick up hover styling (clicks stay
blocked natively via the disabled <button>).
The select positioner painted an opaque square that flashed behind the popup as
it scaled, and multi-select options nested a real focusable checkbox inside each
row, adding a stray tab stop. Drop the positioner background and use a
presentational check indicator; the trigger shows a static up/down caret to
signal it opens a menu.
A chip with onClick rendered as a <span onClick>: no keyboard
activation, not in the tab order, no focus ring, and cursor: default
even though it was clickable. Render a <button type="button"> when
onClick is passed and the chip isn't dismissible (a button containing
the dismiss button would be invalid nested-interactive HTML, so
dismissible chips keep the span). Interactive chips get cursor:
pointer, inherit the app font, and a focus-visible accent ring.
The Field error popped in with no transition and replaced the description in one
jarring jump, and the DatePicker set an error that was never shown. Cross-fade
the error in over the description so the swap is smooth, and render the
DatePicker error as reserved-space helper text below the input.
…es in

The 2D area thumb was positioned via left/top on every pointermove,
forcing layout on each frame of a drag. Drive it from two CSS custom
properties (--thumb-x/--thumb-y, unitless 0..1 fractions set from JS)
composed with container query units and the existing -50%
self-centering transform -- compositor-only, no transition on
transform so the drag stays 1:1 with the pointer.

Also add touch-action: none / user-select: none to .selectionRoot
(the sliders already had this; the 2D area didn't, so dragging on
touch also scrolled the page), and a fast opacity transition on the
thumb so its first-render reveal fades instead of popping.
.nav-text declared an opacity transition, but the collapsed state set
display: none, which can't transition and short-circuited it -- labels
popped out of existence while the frame animated its width. Same for
any consumer content marked data-collapse-hidden. Replace display:none
with opacity + visibility (visibility keeps collapsed content out of
the accessibility tree and unfocusable, same intent as display:none,
but participates in the transition).

Also fix the resize handle's cursor: it only click-toggles collapse,
but showed ew-resize, promising a drag that doesn't exist. Use pointer
on both left- and right-positioned sidebars (per the audit's explicit
decision not to implement drag-to-resize).
Uncontrolled dismiss returned null on the next render, so the callout
vanished in one frame and content below it snapped up by its full
height. There was no enter transition either.

Wrap the callout in a transition shell driven by a data-state attribute
(open/closing/closed). Dismiss now plays a 200ms fade + grid-row height
collapse before unmounting, so content below glides up instead of
snapping. Mount fades in over the same duration. Reduced motion is
instant both ways. The controlled API (onDismiss provided) is
unchanged -- the consumer still owns removal.
Tooltip animated with one-shot @Keyframes: not interruptible (restarts
from zero instead of retargeting), no exit at all (popup unmounted
instantly), a dead unused fade keyframe, and a dead transform-origin
(every keyframe was a pure translate, origin-independent).

Replace with [data-starting-style]/[data-ending-style] transitions at
--rs-duration-fast/--rs-ease-out, keeping the exact per-side and
per-align 2px directional offsets the keyframes used. Enter and exit
now mirror each other, rapid hover toggling retargets smoothly instead
of restarting, data-instant still skips the animation via
transition-duration: 0, and reduced motion gets an opacity-only fade
(previously nothing, since every rule lived inside the no-preference
media query).
Four independent gaps:
- Toast starting-style set a transform but no opacity: 0, so under
  reduced motion (where the transform never transitions) toasts
  popped in at full opacity instead of fading.
- Navbar's hide-on-scroll transform had no reduced-motion fallback at
  all, so the bar teleported in/out; add an additive `reduce` block
  that fades it instead.
- Spinner poles had no static opacity, so reduced motion (where the
  spin animation never applies) rendered a static asterisk instead of
  a graded ring that still reads as loading.
- Dialog and command's backdrop blur had no prefers-reduced-transparency
  guard; add one with a heavier solid scrim (alert-dialog shares
  dialog's stylesheet, so it's covered too).
…uced motion

The popup kept its 450ms transform transition active while swiping, so
it eased toward the finger instead of tracking it directly (the
backdrop already zeroed its own transition). Add transition: none on
[data-swiping].

The five swipe-strength exit calcs hardcoded 400ms while enter runs
450ms via --rs-duration-drawer -- non-swipe exits ran measurably
faster than opens for no reason. Use the token so enter/exit match and
retuning the drawer duration doesn't require finding five literals.

The panel had no transition outside the motion media query, so under
prefers-reduced-motion it popped open/closed instantly. Add a base
opacity fade (with matching [data-starting-style]/[data-ending-style]
opacity: 0) so reduced motion gets a fade instead of a teleport, and
move the per-side slide transforms into the no-preference block
alongside the transition they animate.
ravisuhag added 10 commits July 13, 2026 02:58
The input had zero focus, hover, or invalid styling -- a focused field
looked identical to an idle one. Mirror Field's states verbatim
(hover/focus border+background, invalid border) and add transitions to
both the input and the steppers, which previously snapped instantly.

Stepper :active and :focus-visible already landed via the library-wide
passes (plans 004/005); this plan's remaining gap was the missing
transitions plus the input's hover/invalid states and the input:focus
background-color.
value={null} (indeterminate) rendered a frozen empty track since
nothing styled it. Add a static dimmed fallback plus a looping sweep
(linear) / rotation (circular) gated behind prefers-reduced-motion,
keyed off Base UI's data-indeterminate attribute.

Also move both Progress and Meter's linear fill from a transitioned
width to transform: scaleX(), driven by the existing
--rs-progress-percentage / --rs-meter-percentage variables — the
indicator now renders full-width and Base UI's inline width is
neutralized by an explicit style prop, so value changes composite
instead of triggering layout.
- toolbar.module.css: background-color: none is not valid CSS and was
  silently dropped; use transparent.
- text: the strikeThrough+underline compound variant class was named
  text-italic-strike-through but applies no italic (it merges the two
  decoration lines so they don't cancel out). Rename to
  text-underline-strike-through rather than adding italic, which would
  wrongly italicize any underlined+struck-through text.
The Ordering control's sort icon rendered unsized and swapped between two
separate glyphs for ascending/descending. Give it an explicit size and flip a
single icon vertically around its own centre, so a change of direction reads as
one state turning over rather than the icon being replaced.
The reset setTimeout was never cleared, so rapid clicks stacked timers
and the check icon could revert early; it also fired after unmount.
Hold the timer in a ref, clear/restart it per click and on unmount.

Replace the boolean copied flag with an idle/copied/error status.
Stack all three icons and cross-fade via opacity (plus a subtle scale
under motion) instead of a hard swap, and add a visually hidden
role="status" region announcing "Copied"/"Copy failed".

CopyButton now always mounts CheckCircleFilledIcon and
CrossCircleFilledIcon (previously only Check rendered, conditionally).
Both are SVG-asset icons that only transform via the rollup build's
@svgr plugin, not under vitest, so two consumer tests (code-block,
color-picker) that render CopyButton needed their icon mocks extended
to cover the new icon — same pattern code-block's test already used.
Many controls stripped the outline (all: unset, outline: none/0) with
nothing in its place, leaving keyboard users unable to see where focus
is: tabs, collapsible, checkbox, radio, switch, toggle, slider thumb,
color-picker thumbs, icon-button, link, borderless input/text-area,
chip/filter-chip dismiss buttons, number-field, code-block's floating
copy button, and breadcrumb links/dropdown trigger.

Apply three house treatments by element type: an accent ring for
controls (upgrading the existing faint 1px button ring to 2px with
offset), a subtle background for nav-style breadcrumb items (the
sidebar pattern from #808), and a soft box-shadow ring for chromeless
text-entry surfaces. Also split accordion's focus from its hover
(audit #52) and add otp-field's soft ring on top of its border shift
(audit #53/#58).

Plans: plans/005-focus-indicators-pass.md
…s tier

--rs-transition-interactive hardcoded 0.2s ease instead of composing the
duration/easing tokens declared a few lines below it, and applied the
same 200ms to :active — so a button press arrived as slowly as a hover
fade. Compose the token, add --rs-duration-press (100ms) and
--rs-transition-pressed, and wire it into button/icon-button :active.

Also replace every hardcoded duration/easing across 14 component
stylesheets with the matching --rs-duration-*/--rs-ease-* token, and
sync tour's FADE_OUT_MS (160, matching nothing) to 150ms, the actual
.spotlightCover fade duration.
Consolidates hand-typed easing curves and durations into shared
--rs-ease-*/--rs-duration-* tokens, and fixes a batch of motion issues
across the library: removes the command palette's open/close animation
(keyboard-driven, 100+ uses/day), rebuilds popover/menu/select/combobox
entrances as interruptible origin-anchored transitions, fixes a toast
transition that fought active swipe gestures, replaces `transition: all`
with the interactive token in four components, syncs switch/checkbox/
radio color transitions with their movement, fixes an inverted
reduced-motion override in the sidebar, moves the tabs indicator off
layout properties onto `translate`, and adds missing entrance motion to
checkbox/radio/empty-state.
Use a non-rotating up/down caret on the select trigger and a plain down chevron
on the combobox input, and align the command palette overlay class naming.
Remove the unused eslint config and the np release-tool config and devDeps
(releases run through pnpm publish), and refresh the contributing/development docs.
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Jul 12, 2026 9:59pm

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 33 minutes

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3d898220-59a0-4335-acb8-c0be1268718a

📥 Commits

Reviewing files that changed from the base of the PR and between 5d66454 and 8cfee06.

📒 Files selected for processing (23)
  • packages/raystack/README.md
  • packages/raystack/components/button/button.module.css
  • packages/raystack/components/checkbox/checkbox.module.css
  • packages/raystack/components/code-block/__tests__/code-block.test.tsx
  • packages/raystack/components/command/command.module.css
  • packages/raystack/components/dialog/dialog.module.css
  • packages/raystack/components/drawer/drawer.module.css
  • packages/raystack/components/empty-state/empty-state.module.css
  • packages/raystack/components/field/field.module.css
  • packages/raystack/components/menu/menu.module.css
  • packages/raystack/components/meter/meter.module.css
  • packages/raystack/components/preview-card/preview-card.module.css
  • packages/raystack/components/progress/progress.module.css
  • packages/raystack/components/radio/radio.module.css
  • packages/raystack/components/select/select.module.css
  • packages/raystack/components/sidebar/sidebar.module.css
  • packages/raystack/components/tabs/tabs.module.css
  • packages/raystack/components/text-area/text-area.module.css
  • packages/raystack/components/tooltip/tooltip.module.css
  • packages/raystack/components/tour-beta/tour.module.css
  • packages/raystack/components/tour/tour.module.css
  • packages/raystack/styles/effects.css
  • packages/raystack/styles/typography.css
📝 Walkthrough

Walkthrough

This PR refreshes Raystack’s design-system foundations and component behavior. It adds shared motion, focus-ring, pressed-state, and typography tokens; updates documentation and release commands; improves loading, fallback, dismissal, selection, and indicator lifecycles; and standardizes interactive CSS across controls, overlays, navigation, tables, forms, and feedback components. Several animations move to tokenized transitions with reduced-motion handling, while tests are updated for callout, chip, image, field, code-block, and text behavior.

Possibly related issues

Suggested reviewers: paansinghcoder, rsbh

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the PR’s main design-system audit work on motion, interaction, focus, and accessibility.
Description check ✅ Passed The description is directly related and accurately reflects the broad component-library changes in the changeset.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (8)
packages/raystack/components/tabs/tabs.module.css (1)

115-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent fallback units for translate custom properties.

Line 117 uses unitless 0 fallbacks while lines 159–160 use 0px. Both are valid CSS, but aligning them improves consistency.

✏️ Proposed fix
   translate: var(--active-tab-left, 0) var(--active-tab-top, 0);
+  /* or for consistency with the plain variant: */
+  translate: var(--active-tab-left, 0px) var(--active-tab-top, 0px);

Also applies to: 159-160

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/tabs/tabs.module.css` around lines 115 - 117,
Align the fallback values in the translate declarations by replacing the
unitless 0 defaults for --active-tab-left and --active-tab-top with 0px,
matching the corresponding declarations at lines 159–160.
packages/raystack/components/sidebar/sidebar.module.css (1)

395-417: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use shared motion tokens instead of hardcoded 0.2s ease in the prefers-reduced-motion block.

Within the same media query, .nav-text uses var(--rs-duration-normal) and var(--rs-ease-out), but .root, .nav-group-panel, and [data-collapse-hidden] still use hardcoded 0.2s ease. The PR objective is to consolidate motion tokens — these should be aligned. Note that ease--rs-ease-out (cubic-bezier(0.22, 1, 0.36, 1)), so the easing curves differ too.

♻️ Proposed fix to use shared tokens
 `@media` (prefers-reduced-motion: no-preference) {
   .root {
-    transition: width 0.2s ease;
+    transition: width var(--rs-duration-normal) var(--rs-ease-out);
   }

   .nav-group-panel {
     transition:
-      width 0.2s ease,
-      height 0.2s ease;
+      width var(--rs-duration-normal) var(--rs-ease-out),
+      height var(--rs-duration-normal) var(--rs-ease-out);
   }

   .nav-text {
     transition:
       max-width var(--rs-duration-normal) var(--rs-ease-out),
       opacity var(--rs-duration-normal) var(--rs-ease-out),
       visibility var(--rs-duration-normal) var(--rs-ease-out);
   }

   .root [data-collapse-hidden] {
     transition:
-      max-width 0.2s ease,
-      opacity 0.2s ease,
-      visibility 0.2s ease;
+      max-width var(--rs-duration-normal) var(--rs-ease-out),
+      opacity var(--rs-duration-normal) var(--rs-ease-out),
+      visibility var(--rs-duration-normal) var(--rs-ease-out);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/sidebar/sidebar.module.css` around lines 395 -
417, Replace the hardcoded 0.2s ease transitions in .root, .nav-group-panel, and
.root [data-collapse-hidden] within the prefers-reduced-motion block with the
shared --rs-duration-normal and --rs-ease-out motion tokens, matching .nav-text.
packages/raystack/components/preview-card/preview-card.module.css (1)

106-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant .positioner transition inside the media query.

The .positioner transition at lines 110–112 (opacity var(--animation-duration) var(--easing)) is identical to the base rule at line 9. Since the media query no longer adds transform or positional edges to .positioner (those moved to .positioner:has(.viewport)), this block is redundant and can be removed to reduce confusion.

♻️ Proposed cleanup
 `@media` (prefers-reduced-motion: no-preference) {
   /* Default mode (no PreviewCard.Viewport): Base UI positions the positioner
      via transform (floating-ui). Don't transition that transform, or the card
      eases toward every reposition and swims behind its anchor. The popup owns
      the entrance (scale below); the positioner only fades — like popover/menu/tooltip. */
-  .positioner {
-    transition: opacity var(--animation-duration) var(--easing);
-  }
-
   .popup {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/preview-card/preview-card.module.css` around
lines 106 - 112, Remove the redundant `.positioner` rule inside the media query
in preview-card.module.css, since its opacity transition is already defined by
the base `.positioner` rule. Leave the viewport-specific positioning rules and
all other media-query behavior unchanged.
packages/raystack/components/otp-field/otp-field.module.css (1)

50-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Merge the duplicate invalid-focus rules. In packages/raystack/components/otp-field/otp-field.module.css, combine the two .otp-field-input[data-invalid]:focus:not([data-readonly]) blocks into one for clarity; keep the current explicit danger box-shadow since there’s no matching danger focus-ring token.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/otp-field/otp-field.module.css` around lines 50
- 56, Merge the two duplicate
.otp-field-input[data-invalid]:focus:not([data-readonly]) rules into a single
CSS block, preserving both the danger border-color and the explicit danger
box-shadow declarations.
packages/raystack/components/chip/chip.tsx (2)

119-140: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Double cast on onClick is a type-safety code smell.

Line 134 uses onClick as unknown as React.MouseEventHandler<HTMLButtonElement> to bridge ComponentProps<'span'> and <button>. Consider widening onClick in ChipProps to MouseEventHandler<HTMLElement> to eliminate the cast, since the handler works for both span and button targets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/chip/chip.tsx` around lines 119 - 140, Widen the
ChipProps onClick type to React.MouseEventHandler<HTMLElement>, then update the
interactive button path in the Chip component to pass onClick directly and
remove the double cast. Preserve the existing span and button click behavior.

61-61: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Accessibility gap: dismissible chips with onClick are not keyboard-accessible.

When both onClick and isDismissible are provided, isInteractive is false, so the chip renders a <span> with onClick but no tabindex, no role="button", and no keyboard event handler. Keyboard users cannot activate the chip's onClick. The PR's accessibility objectives ("accessible clickable chips") make this gap worth addressing.

Consider adding tabindex={0}, role="button", and a key-down handler for Enter/Space on the span when onClick is present but isInteractive is false, or document this as an intentional limitation.

Also applies to: 142-148

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/chip/chip.tsx` at line 61, Update the
non-interactive rendering path in the chip component so a chip with onClick and
isDismissible remains keyboard-accessible: add focusability, button semantics,
and Enter/Space key handling that invokes the existing onClick behavior.
Preserve the current dismissible behavior and the existing isInteractive path
for chips without this combination.
packages/raystack/styles/effects.css (1)

40-51: 🎯 Functional Correctness | 🔵 Trivial

Confirm 0.5px focus-ring width is intentional.

--rs-focus-ring uses a 0.5px outline. This matches the codebase's existing hairline-border convention, but as a keyboard-focus indicator specifically (an explicit accessibility deliverable of this PR), sub-pixel outline widths can round away or become hard to perceive on standard-density (1x) displays depending on the browser/engine. Worth confirming this meets the intended focus-visibility bar across target browsers/DPIs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/styles/effects.css` around lines 40 - 51, Confirm the
intended accessibility behavior of --rs-focus-ring across target browsers and
display densities; if 0.5px can render imperceptibly, increase the outline width
to a reliably visible value while preserving the existing focus-ring styling and
shadow variant.
packages/raystack/components/button/button.module.css (1)

216-218: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider guarding the pressed transform behind prefers-reduced-motion.

The :active scale transform at line 216 is outside the @media (prefers-reduced-motion: no-preference) block, so it applies even when users request reduced motion. While the transition is correctly gated (no animation), the transform itself still shifts the element. For strict WCAG 2.3.3 compliance, consider moving the transform inside the media query or adding a reduced-motion override.

♻️ Proposed refactor: move transform inside the media query
-@media (prefers-reduced-motion: no-preference) {
-  .button {
-    transition: var(--rs-transition-interactive);
-  }
-
-  .button:active {
-    transition: var(--rs-transition-pressed);
-  }
-}
+@media (prefers-reduced-motion: no-preference) {
+  .button {
+    transition: var(--rs-transition-interactive);
+  }
+
+  .button:active:not(:disabled):not(.button-disabled):not(.button-loading) {
+    transform: scale(var(--rs-scale-pressed));
+    transition: var(--rs-transition-pressed);
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/button/button.module.css` around lines 216 -
218, Move the pressed-state transform rule for
.button:active:not(:disabled):not(.button-disabled):not(.button-loading) inside
the existing `@media` (prefers-reduced-motion: no-preference) block, so
reduced-motion users do not experience the scale shift while preserving the
current behavior otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CONTRIBUTING.md`:
- Around line 225-228: Clarify the release candidate instructions around the git
pull command: show the release-branch variant explicitly, such as pulling from
origin release/x.y, instead of leaving “or the release branch” as an ambiguous
comment. Preserve the existing main-branch command for non-release workflows.

In `@packages/raystack/components/announcement-bar/announcement-bar.module.css`:
- Around line 65-67: Update the .action-btn:focus-visible rule to use the shared
--rs-focus-ring and --rs-focus-ring-offset-inset tokens instead of hardcoded
outline values, ensuring focus styling matches the project standard.

In `@packages/raystack/components/avatar/avatar.tsx`:
- Around line 182-192: Update handleLoadingStatusChange to handle the 'error'
status by resetting sawLoadingRef.current to false and setFadeIn to false,
preventing a later cached image from incorrectly fading in after a failed load.

In `@packages/raystack/components/command/command.module.css`:
- Around line 197-204: Restore the enter and exit motion rules for .dialogPopup
in command.module.css, including its transition/animation and
[data-starting-style]/[data-ending-style] state selectors, matching the
established behavior of the other dialog popups.

In `@packages/raystack/components/copy-button/copy-button.module.css`:
- Around line 47-57: Update the .sr-only accessibility utility to replace the
deprecated clip declaration with clip-path: inset(50%), while preserving the
existing visually-hidden behavior and all other declarations.

In `@packages/raystack/components/drawer/drawer.module.css`:
- Around line 21-24: Collapse each of the five multiline calc() expressions in
drawer.module.css—including the transition-duration declarations—onto a single
line so the multiplication operator is not followed by a newline, preserving the
existing values and formatting semantics.

In `@packages/raystack/components/link/link.module.css`:
- Around line 24-27: Update the `.link:hover` rule’s `text-decoration-color`
value from `currentColor` to lowercase `currentcolor` to satisfy the project’s
stylelint `value-keyword-case` rule.

In `@packages/raystack/components/meter/meter.module.css`:
- Around line 58-61: Update the --rs-meter-radius calculation so the division
operator and its operand 2 remain on the same line, resolving the stylelint
scss/operator-no-newline-after violation while preserving the existing
calculation.

In `@packages/raystack/components/progress/progress.module.css`:
- Around line 56-61: Fix the calc() formatting in .circularSvg by keeping the /
operator and its right-hand operand on the same line, while preserving the
existing spacing and calculation.

In `@packages/raystack/components/select/select-trigger.tsx`:
- Line 7: Update the `@radix-ui/react-icons` vi.mock in code-block.test.tsx to
export CaretSortIcon alongside the existing mocked icons, including
ChevronDownIcon, so tests can resolve the select trigger import.

In `@packages/raystack/components/switch/switch.module.css`:
- Around line 100-102: Add transform-origin: right center to the
.switch.small[data-checked]:active:not([data-disabled]) .thumb rule, matching
the non-small checked pressed state while preserving its existing transform.

In `@packages/raystack/components/toast/toast.module.css`:
- Line 69: Remove the empty line immediately before the transition declaration
in the toast stylesheet, keeping z-index and transition as consecutive
declarations so Stylelint passes.

---

Nitpick comments:
In `@packages/raystack/components/button/button.module.css`:
- Around line 216-218: Move the pressed-state transform rule for
.button:active:not(:disabled):not(.button-disabled):not(.button-loading) inside
the existing `@media` (prefers-reduced-motion: no-preference) block, so
reduced-motion users do not experience the scale shift while preserving the
current behavior otherwise.

In `@packages/raystack/components/chip/chip.tsx`:
- Around line 119-140: Widen the ChipProps onClick type to
React.MouseEventHandler<HTMLElement>, then update the interactive button path in
the Chip component to pass onClick directly and remove the double cast. Preserve
the existing span and button click behavior.
- Line 61: Update the non-interactive rendering path in the chip component so a
chip with onClick and isDismissible remains keyboard-accessible: add
focusability, button semantics, and Enter/Space key handling that invokes the
existing onClick behavior. Preserve the current dismissible behavior and the
existing isInteractive path for chips without this combination.

In `@packages/raystack/components/otp-field/otp-field.module.css`:
- Around line 50-56: Merge the two duplicate
.otp-field-input[data-invalid]:focus:not([data-readonly]) rules into a single
CSS block, preserving both the danger border-color and the explicit danger
box-shadow declarations.

In `@packages/raystack/components/preview-card/preview-card.module.css`:
- Around line 106-112: Remove the redundant `.positioner` rule inside the media
query in preview-card.module.css, since its opacity transition is already
defined by the base `.positioner` rule. Leave the viewport-specific positioning
rules and all other media-query behavior unchanged.

In `@packages/raystack/components/sidebar/sidebar.module.css`:
- Around line 395-417: Replace the hardcoded 0.2s ease transitions in .root,
.nav-group-panel, and .root [data-collapse-hidden] within the
prefers-reduced-motion block with the shared --rs-duration-normal and
--rs-ease-out motion tokens, matching .nav-text.

In `@packages/raystack/components/tabs/tabs.module.css`:
- Around line 115-117: Align the fallback values in the translate declarations
by replacing the unitless 0 defaults for --active-tab-left and --active-tab-top
with 0px, matching the corresponding declarations at lines 159–160.

In `@packages/raystack/styles/effects.css`:
- Around line 40-51: Confirm the intended accessibility behavior of
--rs-focus-ring across target browsers and display densities; if 0.5px can
render imperceptibly, increase the outline width to a reliably visible value
while preserving the existing focus-ring styling and shadow variant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 580b812f-9606-4f76-81b2-2530f9e3a3c1

📥 Commits

Reviewing files that changed from the base of the PR and between 75a8a6b and 5d66454.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (93)
  • .eslintrc.js
  • .eslintrs.js
  • CONTRIBUTING.md
  • DEVELOPMENT.md
  • packages/raystack/.np-config.json
  • packages/raystack/components/accordion/accordion.module.css
  • packages/raystack/components/amount/amount.module.css
  • packages/raystack/components/amount/amount.tsx
  • packages/raystack/components/announcement-bar/announcement-bar.module.css
  • packages/raystack/components/avatar/avatar.module.css
  • packages/raystack/components/avatar/avatar.tsx
  • packages/raystack/components/breadcrumb/breadcrumb.module.css
  • packages/raystack/components/button/button.module.css
  • packages/raystack/components/calendar/calendar.module.css
  • packages/raystack/components/calendar/calendar.tsx
  • packages/raystack/components/calendar/date-picker.tsx
  • packages/raystack/components/callout/__tests__/callout.test.tsx
  • packages/raystack/components/callout/callout.module.css
  • packages/raystack/components/callout/callout.tsx
  • packages/raystack/components/checkbox/checkbox.module.css
  • packages/raystack/components/checkbox/checkbox.tsx
  • packages/raystack/components/chip/__tests__/chip.test.tsx
  • packages/raystack/components/chip/chip.module.css
  • packages/raystack/components/chip/chip.tsx
  • packages/raystack/components/code-block/__tests__/code-block.test.tsx
  • packages/raystack/components/code-block/code-block.module.css
  • packages/raystack/components/collapsible/collapsible.module.css
  • packages/raystack/components/color-picker/color-picker-area.tsx
  • packages/raystack/components/color-picker/color-picker.module.css
  • packages/raystack/components/combobox/combobox-input.tsx
  • packages/raystack/components/combobox/combobox.module.css
  • packages/raystack/components/command/command-dialog.tsx
  • packages/raystack/components/command/command.module.css
  • packages/raystack/components/copy-button/copy-button.module.css
  • packages/raystack/components/copy-button/copy-button.tsx
  • packages/raystack/components/data-table/data-table.module.css
  • packages/raystack/components/data-view-beta/data-view.module.css
  • packages/raystack/components/data-view/components/ordering.tsx
  • packages/raystack/components/data-view/data-view.module.css
  • packages/raystack/components/dialog/dialog.module.css
  • packages/raystack/components/drawer/drawer.module.css
  • packages/raystack/components/empty-state/empty-state.module.css
  • packages/raystack/components/field/__tests__/field.test.tsx
  • packages/raystack/components/field/field-root.tsx
  • packages/raystack/components/field/field.module.css
  • packages/raystack/components/filter-chip/filter-chip.module.css
  • packages/raystack/components/floating-actions/floating-actions.module.css
  • packages/raystack/components/headline/headline.module.css
  • packages/raystack/components/icon-button/icon-button.module.css
  • packages/raystack/components/image/__tests__/image.test.tsx
  • packages/raystack/components/image/image.module.css
  • packages/raystack/components/image/image.tsx
  • packages/raystack/components/input/input.module.css
  • packages/raystack/components/link/link.module.css
  • packages/raystack/components/menu/menu.module.css
  • packages/raystack/components/meter/meter-track.tsx
  • packages/raystack/components/meter/meter.module.css
  • packages/raystack/components/navbar/navbar.module.css
  • packages/raystack/components/number-field/number-field.module.css
  • packages/raystack/components/otp-field/otp-field.module.css
  • packages/raystack/components/popover/popover.module.css
  • packages/raystack/components/preview-card/preview-card.module.css
  • packages/raystack/components/progress/progress-track.tsx
  • packages/raystack/components/progress/progress.module.css
  • packages/raystack/components/radio/radio.module.css
  • packages/raystack/components/radio/radio.tsx
  • packages/raystack/components/scroll-area/scroll-area.module.css
  • packages/raystack/components/select/select-item.tsx
  • packages/raystack/components/select/select-trigger.tsx
  • packages/raystack/components/select/select.module.css
  • packages/raystack/components/side-panel/side-panel.module.css
  • packages/raystack/components/sidebar/sidebar.module.css
  • packages/raystack/components/skeleton/skeleton.module.css
  • packages/raystack/components/slider/slider.module.css
  • packages/raystack/components/spinner/spinner.module.css
  • packages/raystack/components/switch/switch.module.css
  • packages/raystack/components/table/table.module.css
  • packages/raystack/components/table/table.tsx
  • packages/raystack/components/tabs/tabs.module.css
  • packages/raystack/components/text-area/text-area.module.css
  • packages/raystack/components/text/__tests__/text.test.tsx
  • packages/raystack/components/text/text.module.css
  • packages/raystack/components/text/text.tsx
  • packages/raystack/components/toast/toast.module.css
  • packages/raystack/components/toggle/toggle.module.css
  • packages/raystack/components/toolbar/toolbar.module.css
  • packages/raystack/components/tooltip/tooltip.module.css
  • packages/raystack/components/tour-beta/tour.module.css
  • packages/raystack/components/tour/tour-root.tsx
  • packages/raystack/components/tour/tour.module.css
  • packages/raystack/package.json
  • packages/raystack/styles/effects.css
  • packages/raystack/styles/typography.css
💤 Files with no reviewable changes (5)
  • packages/raystack/.np-config.json
  • .eslintrc.js
  • .eslintrs.js
  • packages/raystack/components/headline/headline.module.css
  • packages/raystack/package.json

Comment thread CONTRIBUTING.md
Comment on lines +225 to +228
git checkout release/x.y # for release candidate

# Pull latest changes
git pull origin main # or develop
git pull origin main # or the release branch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify the git pull command for release candidate branches.

Line 228 shows git pull origin main with a comment "or the release branch", but if a maintainer is on release/x.y, running this command as-is would merge main into the release branch. The command should explicitly show the release branch variant to avoid accidental cross-branch merges.

📝 Suggested fix
   # Pull latest changes
-  git pull origin main  # or the release branch
+  git pull origin main  # for production release
+  # OR
+  git pull origin release/x.y  # for release candidate
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
git checkout release/x.y # for release candidate
# Pull latest changes
git pull origin main # or develop
git pull origin main # or the release branch
git checkout release/x.y # for release candidate
# Pull latest changes
git pull origin main # for production release
# OR
git pull origin release/x.y # for release candidate
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CONTRIBUTING.md` around lines 225 - 228, Clarify the release candidate
instructions around the git pull command: show the release-branch variant
explicitly, such as pulling from origin release/x.y, instead of leaving “or the
release branch” as an ambiguous comment. Preserve the existing main-branch
command for non-release workflows.

Comment on lines +65 to +67
.action-btn:focus-visible {
outline: 1px solid currentColor;
outline-offset: 2px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use shared focus-ring tokens for consistency and fix currentcolor casing.

Two concerns:

  1. Stylelint flags currentColor — CSS keyword should be lowercase currentcolor per the project's value-keyword-case rule.
  2. The accordion and other components use outline: var(--rs-focus-ring) and outline-offset: var(--rs-focus-ring-offset-inset) for :focus-visible. Using hardcoded values here deviates from the PR's standardization goal.
🔧 Proposed fix
 .action-btn:focus-visible {
-  outline: 1px solid currentColor;
-  outline-offset: 2px;
+  outline: var(--rs-focus-ring);
+  outline-offset: var(--rs-focus-ring-offset-inset);
 }

If currentColor is intentionally desired here (e.g., the announcement bar inherits a contextual text color), keep the approach but fix the casing:

 .action-btn:focus-visible {
-  outline: 1px solid currentColor;
+  outline: 1px solid currentcolor;
   outline-offset: 2px;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.action-btn:focus-visible {
outline: 1px solid currentColor;
outline-offset: 2px;
.action-btn:focus-visible {
outline: var(--rs-focus-ring);
outline-offset: var(--rs-focus-ring-offset-inset);
}
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 66-66: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/announcement-bar/announcement-bar.module.css`
around lines 65 - 67, Update the .action-btn:focus-visible rule to use the
shared --rs-focus-ring and --rs-focus-ring-offset-inset tokens instead of
hardcoded outline values, ensuring focus styling matches the project standard.

Source: Linters/SAST tools

Comment on lines +182 to +192
const handleLoadingStatusChange = (status: ImageLoadingStatus) => {
if (status === 'loading') {
sawLoadingRef.current = true;
setFadeIn(false);
} else if (status === 'loaded') {
// A cached image reports 'loaded' with no 'loading' phase (Base UI never
// emits it), so it stays instant; only network loads fade in.
setFadeIn(sawLoadingRef.current);
sawLoadingRef.current = false;
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle the 'error' loading status to reset fade state.

When the image fails to load (status === 'error'), sawLoadingRef.current is never reset. If a subsequent image is cached (reports 'loaded' without 'loading'), it will incorrectly fade in because the ref is still true from the failed attempt. Add an error branch to reset both the ref and fadeIn.

🐛 Proposed fix for unhandled error status
   const handleLoadingStatusChange = (status: ImageLoadingStatus) => {
     if (status === 'loading') {
       sawLoadingRef.current = true;
       setFadeIn(false);
     } else if (status === 'loaded') {
       // A cached image reports 'loaded' with no 'loading' phase (Base UI never
       // emits it), so it stays instant; only network loads fade in.
       setFadeIn(sawLoadingRef.current);
       sawLoadingRef.current = false;
+    } else if (status === 'error') {
+      sawLoadingRef.current = false;
+      setFadeIn(false);
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleLoadingStatusChange = (status: ImageLoadingStatus) => {
if (status === 'loading') {
sawLoadingRef.current = true;
setFadeIn(false);
} else if (status === 'loaded') {
// A cached image reports 'loaded' with no 'loading' phase (Base UI never
// emits it), so it stays instant; only network loads fade in.
setFadeIn(sawLoadingRef.current);
sawLoadingRef.current = false;
}
};
const handleLoadingStatusChange = (status: ImageLoadingStatus) => {
if (status === 'loading') {
sawLoadingRef.current = true;
setFadeIn(false);
} else if (status === 'loaded') {
// A cached image reports 'loaded' with no 'loading' phase (Base UI never
// emits it), so it stays instant; only network loads fade in.
setFadeIn(sawLoadingRef.current);
sawLoadingRef.current = false;
} else if (status === 'error') {
sawLoadingRef.current = false;
setFadeIn(false);
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/avatar/avatar.tsx` around lines 182 - 192,
Update handleLoadingStatusChange to handle the 'error' status by resetting
sawLoadingRef.current to false and setFadeIn to false, preventing a later cached
image from incorrectly fading in after a failed load.

Comment on lines 197 to +204
}

.dialogPopup[data-starting-style],
.dialogPopup[data-ending-style] {
opacity: 0;
transform: scale(0.98);
/* Reduced transparency: drop the blur, compensate with a heavier solid scrim */
@media (prefers-reduced-transparency: reduce) {
.overlayBlur {
backdrop-filter: none;
background-color: var(--rs-color-overlay-black-a9);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Base UI's Dialog.Popup applies transitions internally
rg -n 'transition|animation|data-starting-style|data-ending-style' \
  node_modules/@base_ui/react/esm/dialog/ 2>/dev/null | head -30

# Also check the dialog.module.css for comparison — does the shared dialog animate its popup?
fd 'dialog.module.css' packages/raystack/components/dialog/ --exec rg -n 'transition|data-starting-style|data-ending-style' {}

Repository: raystack/apsara

Length of output: 589


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== command.module.css around dialogPopup ==="
FILE="packages/raystack/components/command/command.module.css"
nl -ba "$FILE" | sed -n '160,230p'

echo
echo "=== dialog.module.css references to popup/state animations ==="
DIALOG_FILE="$(fd -a 'dialog.module.css' packages/raystack/components/dialog | head -n 1)"
echo "$DIALOG_FILE"
nl -ba "$DIALOG_FILE" | sed -n '1,140p'

echo
echo "=== search for Dialog.Popup / starting-ending style usage in repo ==="
rg -n 'Dialog\.Popup|data-starting-style|data-ending-style|showNestedAnimation|transition: opacity|animation:' packages/raystack/components -g '*.css' -g '*.tsx' -g '*.ts' | sed -n '1,220p'

echo
echo "=== search Base UI dialog package if present in workspace ==="
fd -a 'dialog' node_modules/@base_ui/react 2>/dev/null | head -n 50

Repository: raystack/apsara

Length of output: 239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== command.module.css around dialogPopup ==="
sed -n '160,230p' packages/raystack/components/command/command.module.css

echo
echo "=== shared dialog.module.css around animation rules ==="
DIALOG_FILE="$(fd -a 'dialog.module.css' packages/raystack/components/dialog | head -n 1)"
echo "$DIALOG_FILE"
sed -n '1,140p' "$DIALOG_FILE"

echo
echo "=== repo search for dialog popup animation/state hooks ==="
rg -n 'dialogPopup|Dialog\.Popup|data-starting-style|data-ending-style|showNestedAnimation|transition: opacity|animation:' packages/raystack/components -g '*.css' -g '*.tsx' -g '*.ts'

Repository: raystack/apsara

Length of output: 20842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== command-dialog.tsx ==="
sed -n '1,160p' packages/raystack/components/command/command-dialog.tsx

echo
echo "=== command.module.css dialogPopup block ==="
sed -n '175,220p' packages/raystack/components/command/command.module.css

echo
echo "=== Base UI dialog CSS matches already present in workspace ==="
rg -n 'dialogOverlay|dialogContent|data-starting-style|data-ending-style|transition: opacity' node_modules/@base_ui/react/esm/dialog/ 2>/dev/null | sed -n '1,120p'

Repository: raystack/apsara

Length of output: 2689


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== components using DialogPrimitive.Popup ==="
rg -n 'DialogPrimitive\.Popup|DialogPrimitive\.Backdrop|DialogPrimitive\.Viewport' packages/raystack/components -g '*.tsx'

echo
echo "=== dialog-like CSS files with Popup/data-starting-style rules ==="
rg -n '(\.popup|\.dialogContent|\.drawerPopup|data-starting-style|data-ending-style)' packages/raystack/components -g '*.css' | sed -n '1,220p'

Repository: raystack/apsara

Length of output: 14598


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== dialog-content.tsx ==="
sed -n '1,120p' packages/raystack/components/dialog/dialog-content.tsx

echo
echo "=== alert-dialog-content.tsx ==="
sed -n '1,120p' packages/raystack/components/alert-dialog/alert-dialog-content.tsx

echo
echo "=== alert-dialog.module.css if present ==="
fd -a 'alert-dialog.module.css' packages/raystack/components | while read -r f; do
  echo "$f"
  sed -n '1,220p' "$f"
done

Repository: raystack/apsara

Length of output: 3416


Restore dialogPopup enter/exit styles

packages/raystack/components/command/command.module.css:183-204 no longer defines any transition, animation, or [data-starting-style]/[data-ending-style] rules for .dialogPopup, so the command dialog now opens/closes without motion. Add the popup enter/exit styles back if this should match the other dialog popups.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/command/command.module.css` around lines 197 -
204, Restore the enter and exit motion rules for .dialogPopup in
command.module.css, including its transition/animation and
[data-starting-style]/[data-ending-style] state selectors, matching the
established behavior of the other dialog popups.

Comment on lines +47 to +57
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace deprecated clip with clip-path.

Stylelint flags clip: rect(0, 0, 0, 0) as deprecated. Use the modern clip-path: inset(50%) equivalent.

♻️ Proposed fix
 .sr-only {
   position: absolute;
   width: 1px;
   height: 1px;
   padding: 0;
   margin: -1px;
   overflow: hidden;
-  clip: rect(0, 0, 0, 0);
+  clip-path: inset(50%);
   white-space: nowrap;
   border: 0;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 54-54: Deprecated property "clip" (property-no-deprecated)

(property-no-deprecated)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/copy-button/copy-button.module.css` around lines
47 - 57, Update the .sr-only accessibility utility to replace the deprecated
clip declaration with clip-path: inset(50%), while preserving the existing
visually-hidden behavior and all other declarations.

Source: Linters/SAST tools

Comment on lines +58 to +61
--rs-meter-radius: calc(
(var(--rs-space-14) - var(--rs-meter-track-size) * 2) /
2
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix stylelint scss/operator-no-newline-after error: keep / and 2 on the same line.

Stylelint reports Unexpected newline after "/" at line 59. The / operator should not be followed by a newline.

🛡️ Proposed fix
 .circularSvg {
   --rs-meter-track-size: var(--rs-space-2);
   --rs-meter-radius: calc(
-    (var(--rs-space-14) - var(--rs-meter-track-size) * 2) /
-    2
+    (var(--rs-space-14) - var(--rs-meter-track-size) * 2) / 2
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
--rs-meter-radius: calc(
(var(--rs-space-14) - var(--rs-meter-track-size) * 2) /
2
);
--rs-meter-radius: calc(
(var(--rs-space-14) - var(--rs-meter-track-size) * 2) / 2
);
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 59-59: Unexpected newline after "/" (scss/operator-no-newline-after)

(scss/operator-no-newline-after)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/meter/meter.module.css` around lines 58 - 61,
Update the --rs-meter-radius calculation so the division operator and its
operand 2 remain on the same line, resolving the stylelint
scss/operator-no-newline-after violation while preserving the existing
calculation.

Source: Linters/SAST tools

Comment on lines 56 to +61
.circularSvg {
--rs-progress-track-size: var(--rs-space-2);
--rs-progress-radius: calc((var(--rs-space-14) - var(--rs-progress-track-size) * 2) / 2);
--rs-progress-radius: calc(
(var(--rs-space-14) - var(--rs-progress-track-size) * 2) /
2
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix stylelint error: newline after / operator in calc().

Stylelint reports scss/operator-no-newline-after on line 59. The / operator should not be followed by a newline. This is a linting error that could fail CI.

🔧 Proposed fix
   --rs-progress-radius: calc(
-    (var(--rs-space-14) - var(--rs-progress-track-size) * 2) /
-    2
+    (var(--rs-space-14) - var(--rs-progress-track-size) * 2) / 2
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.circularSvg {
--rs-progress-track-size: var(--rs-space-2);
--rs-progress-radius: calc((var(--rs-space-14) - var(--rs-progress-track-size) * 2) / 2);
--rs-progress-radius: calc(
(var(--rs-space-14) - var(--rs-progress-track-size) * 2) /
2
);
.circularSvg {
--rs-progress-track-size: var(--rs-space-2);
--rs-progress-radius: calc(
(var(--rs-space-14) - var(--rs-progress-track-size) * 2) / 2
);
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 59-59: Unexpected newline after "/" (scss/operator-no-newline-after)

(scss/operator-no-newline-after)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/progress/progress.module.css` around lines 56 -
61, Fix the calc() formatting in .circularSvg by keeping the / operator and its
right-hand operand on the same line, while preserving the existing spacing and
calculation.

Source: Linters/SAST tools

Comment thread packages/raystack/components/select/select-trigger.tsx
Comment on lines +100 to +102
.switch.small[data-checked]:active:not([data-disabled]) .thumb {
transform: translateX(var(--rs-space-4)) scaleX(1.15);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing transform-origin on small checked pressed state.

.switch.small[data-checked]:active:not([data-disabled]) .thumb omits transform-origin: right center, which is present on the non-small checked variant (line 97). Without it, the default 50% 50% applies and the thumb stretches from center rather than from the right edge — inconsistent with the iOS-like behavior described in the comment on line 87.

✏️ Proposed fix
 .switch.small[data-checked]:active:not([data-disabled]) .thumb {
   transform: translateX(var(--rs-space-4)) scaleX(1.15);
+  transform-origin: right center;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.switch.small[data-checked]:active:not([data-disabled]) .thumb {
transform: translateX(var(--rs-space-4)) scaleX(1.15);
}
.switch.small[data-checked]:active:not([data-disabled]) .thumb {
transform: translateX(var(--rs-space-4)) scaleX(1.15);
transform-origin: right center;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/switch/switch.module.css` around lines 100 -
102, Add transform-origin: right center to the
.switch.small[data-checked]:active:not([data-disabled]) .thumb rule, matching
the non-small checked pressed state while preserving its existing transform.

z-index: calc(1000 - var(--toast-index));

transition: opacity 500ms;
transition: opacity var(--rs-duration-slow) var(--rs-ease-out);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the blank line before this declaration to satisfy Stylelint.

The empty line at line 68 (between z-index and transition) trips declaration-empty-line-before and will fail lint/CI.

🧹 Proposed fix
   z-index: calc(1000 - var(--toast-index));
-
   transition: opacity var(--rs-duration-slow) var(--rs-ease-out);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
transition: opacity var(--rs-duration-slow) var(--rs-ease-out);
z-index: calc(1000 - var(--toast-index));
transition: opacity var(--rs-duration-slow) var(--rs-ease-out);
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 69-69: Expected no empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/raystack/components/toast/toast.module.css` at line 69, Remove the
empty line immediately before the transition declaration in the toast
stylesheet, keeping z-index and transition as consecutive declarations so
Stylelint passes.

Source: Linters/SAST tools

The code-block language selector renders a Select, whose trigger now uses
CaretSortIcon; add it to the test's @radix-ui/react-icons mock.
…README

- Gate a handful of ungated transforms/animations behind
  prefers-reduced-motion so reduced-motion users get a plain fade like the
  rest of the library: dialog nested-scale, empty-state entrance, preview-card
  morph, sidebar chevron, and the tour cards.
- Remove dead and redundant rules across components: unused classes
  (menu/meter/progress/tooltip), no-op hover/focus/disabled overrides,
  duplicated declarations, and a stale commented-out block.
- Consolidate the duplicated light-theme shadow tokens into one grouped
  selector so they cannot drift.
- Rewrite the package README.
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.

Consistent focus-visible ring styles [Link] Component improvements [Image] Add error/loading state handling

1 participant