Rams MCP · The full engine, now in your coding agent
picocss on GitHub

picocss/pico

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

30 files reviewed·July 14, 2026

View on GitHub

Elevated

Design risk in this codebase.

13issues
Critical & Serious · top 6 shown below

Top fix

Remove the :not([role]) gate blocking focus-visible outlines

See the fix

Verdict

Token discipline is excellent almost everywhere, but that polish evaporates the instant JS enhances markup with role="button", stripping focus rings entirely. The biggest risk is keyboard users losing all visual feedback on accordions, exactly where they need it most.

Files Rams reviewed

scss/components/_accordion.scss

scss/components/_card.scss

scss/components/_dropdown.scss

scss/components/_group.scss

scss/components/_loading.scss

scss/components/_modal.scss

scss/components/_nav.scss

scss/components/_progress.scss

scss/components/_tooltip.scss

scss/_index.scss

scss/_settings.scss

scss/colors/utilities/_background-colors.scss

scss/colors/utilities/_colors.scss

scss/colors/utilities/_css-vars.scss

scss/colors/utilities/_settings.scss

scss/colors/utilities/_utils.scss

scss/content/_button.scss

scss/content/_code.scss

scss/content/_embedded.scss

scss/content/_link.scss

scss/content/_misc.scss

scss/content/_table.scss

scss/content/_typography.scss

scss/forms/_basics.scss

scss/forms/_checkbox-radio-switch.scss

scss/forms/_input-date.scss

scss/forms/_input-file.scss

scss/forms/_input-range.scss

scss/forms/_input-search.scss

scss/helpers/_functions.scss

91/100

Accessibility

1 critical3 serious
AccessibilityCritical

scss/components/_accordion.scss:66

role="button" accordion summaries lose all focus indication

The `summary:focus` rule sets `outline: none` unconditionally (line 59), and the replacement ring in `summary:focus-visible` is scoped to `&:not([role])` (lines 67-71). The JS-enhanced accordion trigger uses `summary[role="button"]` (line 75), so that variant hits the `outline: none` reset but never qualifies for the `:not([role])` restoration. - Native `<summary>` (no role): gets the focus-visible ring back - `summary[role="button"]`: outline removed, ring never restored Keyboard users tabbing to a `role="button"` accordion header see no focus indicator at all.

Why it matters

Keyboard-only users can't tell which accordion header is focused, so they lose their place in the list and can't reliably operate the control. This is the documented button variant of the component, not an edge case.

Fix

Apply the focus-visible outline to all summary elements regardless of role, and only gate the color change behind `:not([role])`.

&:focus-visible {
  &:not([role]) {
    outline: var(#{$css-var-prefix}outline-width) solid var(#{$css-var-prefix}primary-focus);
    outline-offset: calc(var(#{$css-var-prefix}spacing, 1rem) * 0.5);
    color: var(#{$css-var-prefix}primary);
  }
}
&:focus-visible {
  outline: var(#{$css-var-prefix}outline-width) solid var(#{$css-var-prefix}primary-focus);
  outline-offset: calc(var(#{$css-var-prefix}spacing, 1rem) * 0.5);

  &:not([role]) {
    color: var(#{$css-var-prefix}primary);
  }
}
AccessibilitySerious

scss/components/_group.scss:87

Button focus indicator is fully delegated to the group's shadow variable

Inside the `:has()` block, focused buttons get `box-shadow: none` on themselves and rely entirely on `#{$css-var-prefix}group-box-shadow-focus-with-button` set on the parent group for any visible focus feedback.

Why it matters

If that group-level shadow variable ever resolves to a low-contrast or unset value in a given theme, the button loses its own focus indicator with nothing left to fall back on, and keyboard users lose track of where focus is.

Fix

Keep a minimal fallback box-shadow or outline on the button itself so focus is never fully dependent on a single external variable resolving correctly.

button,
[type="submit"],
[type="reset"],
[type="button"],
[role="button"] {
  &:focus {
    box-shadow: none;
  }
}
button,
[type="submit"],
[type="reset"],
[type="button"],
[role="button"] {
  &:focus {
    box-shadow: var(#{$css-var-prefix}group-box-shadow-focus-with-button, 0 0 0 1px currentColor);
  }
}
AccessibilitySerious

scss/components/_loading.scss:38

Busy buttons block clicks but keyboard users can still submit twice

The busy-state rule for buttons and links only sets `pointer-events: none` on `[aria-busy="true"]`: - `button`, `[type="submit"]`, `[type="button"]`, `[type="reset"]`, `[role="button"]`, `a` `pointer-events: none` blocks mouse hit-testing, but a focused button still fires a synthetic click on Enter/Space because that path doesn't go through pointer hit-testing. A keyboard user tabbed onto the button before the busy state started can activate it again while a request is in flight.

Why it matters

Double-submit protection silently fails for keyboard-only and switch-device users, the exact audience most likely to trigger it since they can't see a changed cursor as a cue to stop.

Fix

Pair the CSS block with a real `disabled`/`aria-disabled` attribute set alongside `aria-busy` in markup, since CSS alone cannot stop keyboard activation.

&[aria-busy="true"] {
  pointer-events: none;
}
&[aria-busy="true"] {
  cursor: not-allowed;
  pointer-events: none;
  // Pair with disabled or aria-disabled="true" in markup —
  // pointer-events cannot block Enter/Space activation on a focused control
}
AccessibilitySerious

scss/components/_accordion.scss:14

Accordion trigger's clickable area is under the 24px touch minimum

The `summary` element sets `line-height: 1rem` (line 14) with no padding anywhere in the ruleset, so the entire interactive header (the accordion's only trigger) resolves to roughly 16px tall.

Why it matters

16px falls below the WCAG 2.2 AA 24x24px target size floor (2.5.8), making the accordion trigger hard to tap accurately on mobile and difficult for users with motor impairments.

Fix

Add vertical padding to the summary so its total hit area clears 24px.

line-height: 1rem;
line-height: 1rem;
padding-block: 0.375rem;
98/100

Color

1 serious
ColorSerious

scss/components/_dropdown.scss:65

Selected dropdown value is styled like empty placeholder text

`#{$parent-selector} details.dropdown > summary:not([role])` (the select-like trigger) sets `color: var(#{$css-var-prefix}form-element-placeholder-color);` with no separate rule for a chosen value.

Why it matters

Placeholder color exists to signal 'nothing entered yet.' Using it as the permanent label color makes a picked option look indistinguishable from an unselected field, so users can't tell at a glance whether they've made a choice.

Fix

Use the standard form-element text color for the summary label and reserve the placeholder token for genuinely empty states.

color: var(#{$css-var-prefix}form-element-placeholder-color);
color: var(#{$css-var-prefix}form-element-color, var(#{$css-var-prefix}form-element-placeholder-color));

Want this on your repo?

Rams reviews your next PR automatically and posts inline fix suggestions.

Review my public repo for free
98/100

UX

1 serious
UXSerious

scss/components/_accordion.scss:22

Button-role accordion header never changes color across states

The close, active, and open summary colors are all gated behind `&:not([role])`: - close state, line 22 - active/focus state, line 61 - open state, line 91 A `summary[role="button"]` header (the JS-enhanced accordion pattern defined at line 75) never matches any of these, so its text color is identical whether it's closed, focused, or expanded. Only the chevron rotation communicates state.

Why it matters

Users relying on the marker rotation alone for state feedback get a weaker signal than sighted users of the native variant, and any user who misses the small chevron flip has no other cue the section opened.

Fix

Apply the close-state color regardless of role so the button variant gets the same baseline feedback as the native summary.

&:not([role]) {
  color: var(#{$css-var-prefix}accordion-close-summary-color);
}
color: var(#{$css-var-prefix}accordion-close-summary-color);

Typography

No issues found

Spacing

No issues found

Components

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • The spinner icon and spacing both pull from CSS custom properties (`var(#{$css-var-prefix}icon-loading)`, `var(#{$css-var-prefix}spacing)`) instead of hardcoded values, so the loading indicator inherits theme changes automatically rather than drifting from the rest of the system.
  • Every color value routes through a CSS custom property (`var(#{$css-var-prefix}accordion-close-summary-color)`, `var(#{$css-var-prefix}primary-focus)`, etc.) with no hardcoded hex anywhere in the file. That's what makes the theme swappable without touching this component.
  • RTL is handled as a dedicated `[dir="rtl"]` override block that flips `text-align`, `float`, and `background-position` together (lines 104-115), rather than scattering logical-property patches through the base rules. That keeps the LTR rules easy to read on their own.
  • The base group rule builds its box-shadow, border-radius, spacing, and transition entirely from `var(#{$css-var-prefix}...)` tokens, which is why the 5rem pill radius stands out as the one place someone hand-typed a number instead of reaching for the token layer.

Score your own repo with Rams.

Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.