[Feature] AspectRatio: match shadcn (Base UI + vega)

Merged
CI
3d ago
+265-55

Rebuilds AspectRatio after shadcn's Base UI + vega AspectRatio. With these changes the shadcn audit reports no Look, HTML, Props, Behavior or Native differences.

The audit fixture renders the same pixels on main, on this branch and in shadcn, in light and dark. What you can see changes on the docs page: the examples follow shadcn's (rounded-lg bg-muted, no border or shadow), and there is a new "Content taller than the ratio" example. Card and Dialog look the same as on main.

Aspect Ratio docs, light
Aspect Ratio docs, dark
Card docs, light
Dialog docs, dark

Changes

  • AspectRatio renders one <div data-slot="aspect-ratio"> with style="--ratio: N" and relative aspect-(--ratio), the CSS aspect-ratio property. Before, it rendered a wrapper with a padding-bottom percentage and an inner absolute inset-0 div.
  • Caller attrs (class, id, data, style) now land on that div. Before, they went to the inner div, so classes such as max-w-*, mx-auto or overflow-hidden acted on a different element than in shadcn.
  • New ratio: kwarg. It takes a Numeric (16.0 / 9) or a "W/H" String, and keeps decimals: "2.39/1" used to render as 2/1. The default stays 16/9. aspect_ratio: still works as a deprecated alias (it emits a :deprecated warning). Invalid ratios raise ArgumentError.
  • style: as a String or a Hash is merged with --ratio.
  • Content taller than the ratio grows the box, as in shadcn. Add overflow-hidden to keep the ratio.
  • Docs use ratio: and give images their own classes, plus a new "Content taller than the ratio" example. The Card and Dialog examples and the themes grid card are updated the same way.
  • AspectRatio is now part of the shadcn audit (audit/component_map.yml, audit/fixtures/aspect_ratio/).

Possible breaking changes for apps that copied the component:

  • No default bg-muted background. Pass class: "bg-muted" to keep it.
  • No [&>img] styles. A direct <img> child no longer fills the box on its own; give it class: "absolute inset-0 size-full object-cover".
  • Content taller than the ratio now grows the box instead of overflowing it.

Testing instructions

  1. cd gem && bundle exec rake
  2. cd audit && bundle exec rake
  3. cd docs && bin/dev, then open /docs/aspect_ratio, /docs/card and /docs/dialog. The images fill their boxes at the right ratio, and the "Content taller than the ratio" example clips its paragraph.
  4. Optional: run /shadcn-audit aspect_ratio.
shadcn audit, main
shadcn audit  ruby-ui/ruby_ui
  base 8221400d  head 8221400d  shadcn c0d63c33
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (light)
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (dark)
⚠️ Environment  RubyUI preview booted without mise exec (mise resolved Ruby 4.0.9 in the checkout) and with date bumped 3.5.1 → 3.6.0 in the throwaway docs/Gemfile.lock (a globally activated date 3.6.0 blocked bin/rails); no effect on rendered output
✅ Theme  neutral tokens match shadcn
AspectRatio  (ref: bases/base/ui/aspect-ratio.tsx + style-vega.css @ c0d63c33)
  ❌ Look       background-color: rgba(0, 0, 0, 0); border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; border-top-left-radius: 0px; border-top-right-radius: 0px; padding-bottom: 216px
               vega: background-color: oklch(0.269 0 0); background-color: oklch(0.97 0 0); border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; border-top-left-radius: 10px; border-top-right-radius: 10px; padding-bottom: 0px
               AspectRatio — closed/light, closed/dark. Root element: RubyUI <div class="relative w-full" style="padding-bottom: 56.25%"> vs shadcn <div class="relative aspect-(--ratio) rounded-lg bg-muted">. The caller class (rounded-lg bg-muted) lands on RubyUI inner absolute div, not on the root, so the root has no background/radius and keeps the padding-bottom ratio box. Pixel diff is 0 in both themes for this fixture (the inner div covers the root), but caller classes that act on the box itself (border, shadow, max-w-*, overflow-hidden, margin) end up on a different element than in shadcn.
               ↳ bases/base/ui/aspect-ratio.tsx:16 (computed styles, shadcn-audit collect)
  ⏸ Look       properties: font-family
               AspectRatio — explained by environment diff: font
  ✅ ARIA       AspectRatio: accessibility tree matches
               ↳ accessibility tree (shadcn-audit collect)
  ❌ Look       defaults: bg-muted absolute inset-0 [&>img]:object-cover [&>img]:absolute [&>img]:h-full [&>img]:w-full [&>img]:inset-0 [&>img]:text-transparent (gem aspect_ratio.rb:29)
               vega: defaults: relative aspect-(--ratio) only; background, radius and image sizing come from the caller
               With no class, RubyUI paints bg-muted and styles a direct <img> child; shadcn paints nothing. The fixture passes bg-muted on both sides, so collect does not see this default. No style-vega.css rule for aspect-ratio (reference has no cn-* markers).
               ↳ bases/base/ui/aspect-ratio.tsx:16
  ❌ HTML       <div class="relative w-full" style="padding-bottom: N%"><div class="absolute inset-0 …" {attrs}>children</div></div>, no data-slot
               shadcn: <div data-slot="aspect-ratio" style="--ratio: N" class="relative aspect-(--ratio)" {props}>children</div>
               Two elements vs one; caller attrs (class, id, data-*, aria-*) go to the inner div in RubyUI and to the root in shadcn; no data-slot="aspect-ratio". Seen in both rendered DOMs.
               ↳ bases/base/ui/aspect-ratio.tsx:9-17
  ❌ Props      aspect_ratio: "16/9" (String with "/", default "16/9"; raises otherwise; parts go through to_i, so "2.39/1" renders 2/1)
               shadcn: ratio={16 / 9} (number, required)
               Mapped in component_map.yml (ratio → aspect_ratio) but name and type differ: RubyUI cannot take a number (1.5, 16.0/9) and silently truncates decimal parts (gem aspect_ratio.rb:6,24). No rubyui_only kwargs.
               ↳ bases/base/ui/aspect-ratio.tsx:4-7
  ⚠️ Behavior   content taller than the ratio: box stays 384×216, content overflows (inner div is absolute inset-0)
               shadcn: content taller than the ratio: box grows to 384×1776 (aspect-ratio automatic minimum size = min-content)
               Probed with Playwright at 1280×800 (chrome-devtools MCP browser was held by another audit): replaced the img with a long <p>. With an image or short content both are 384×216. Behavior differs, so ⚠️; css-sizing-4 is cited for why shadcn grows, not as a violation.
               ↳ https://drafts.csswg.org/css-sizing-4/#aspect-ratio-minimum
  ❌ Native     padding-bottom percentage hack on a wrapper + absolute inner div (computed in Ruby, inline style)
               shadcn: CSS aspect-ratio property (aspect-(--ratio))
               Candidate for CSS aspect-ratio (no pt-BR MDN page). No Stimulus controller on either side, so this is a CSS hack rather than JS; the native property removes the wrapper, puts attrs on the root and lets content grow as shadcn does.
               ↳ https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/aspect-ratio
  ✅ Spec       generic <div>(s), no role or ARIA attributes
               shadcn observed DOM is a plain div (ARIA in HTML: div, any role allowed, none set); nothing normative to meet. Accessibility tree identical on both sides (img "Photo" from the fixture).
               ↳ https://www.w3.org/TR/html-aria/#el-div
shadcn audit, this PR
shadcn audit  ruby-ui/ruby_ui
  base 8221400d  head 8221400d  shadcn c0d63c33
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (light)
⚠️ Environment  font: "Inter Variable", "system-ui", -apple-system, "system-ui", "Segoe UI", Roboto, "Helvetica Neue", Arial, "sans-serif" vs "Inter Variable", sans-serif (dark)
⚠️ Environment  RubyUI side is the uncommitted worktree on top of 8221400d (preview checkout worktree-aspect-ratio), booted without mise exec and with date 3.6.0 in the throwaway docs/Gemfile.lock
✅ Theme  neutral tokens match shadcn
AspectRatio  (ref: bases/base/ui/aspect-ratio.tsx + style-vega.css @ c0d63c33)
  ⏸ Look       properties: font-family
               AspectRatio — explained by environment diff: font
  ✅ ARIA       AspectRatio: accessibility tree matches
               ↳ accessibility tree (shadcn-audit collect)
  ✅ Look       relative aspect-(--ratio) + caller class on the root; computed styles match in light and dark, pixel diff 0
               No bg-muted or [&>img] defaults any more; collect found no Look difference on the root.
               ↳ bases/base/ui/aspect-ratio.tsx:16
  ✅ HTML       <div data-slot="aspect-ratio" style="--ratio: 1.7777777777777777;" class="relative aspect-(--ratio) …">
               One element, attrs on the root; seen in both rendered DOMs (Playwright probe).
               ↳ bases/base/ui/aspect-ratio.tsx:9-17
  ✅ Props      ratio: Numeric (16.0 / 9) or "W/H" String, decimals kept
               RubyUI keeps a 16/9 default (shadcn requires ratio) and aspect_ratio: as a deprecated alias (Warning category :deprecated). Invalid ratios raise ArgumentError.
               ↳ bases/base/ui/aspect-ratio.tsx:4-7
  ✅ Behavior   content taller than the ratio: box grows 384×216 → 384×1776
               Playwright probe at 1280×800 (chrome-devtools MCP browser held by another audit). Docs show overflow-hidden to keep the ratio.
               ↳ https://drafts.csswg.org/css-sizing-4/#aspect-ratio-minimum
  ✅ Native     CSS aspect-ratio property (aspect-(--ratio)), no JS
               padding-bottom wrapper removed.
               ↳ https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/aspect-ratio
  ✅ Spec       generic <div>, no role or ARIA attributes
               Nothing normative to meet; accessibility tree identical.
               ↳ https://www.w3.org/TR/html-aria/#el-div

🤖 Generated with Claude Code


Summary by cubic

Rebuilds AspectRatio to match shadcn's Base UI + vega rendering. The shadcn audit now reports no Look, HTML, Props, Behavior, or Native differences.

The component renders one <div data-slot="aspect-ratio"> sized with the CSS aspect-ratio property (--ratio plus aspect-(--ratio)), replacing the padding-bottom wrapper and absolute inner div. Caller attrs now land on the root. ratio: takes a Numeric (16.0 / 9) or a "W/H" String and keeps decimals; aspect_ratio: stays as a deprecated alias, and invalid ratios—including false, Complex, or ratios that truncate to non-positive values—raise ArgumentError. Content taller than the ratio now grows the box, matching shadcn.

Migration

  • No default bg-muted background; pass class: "bg-muted" to keep it.
  • A direct <img> child no longer fills the box; give it class: "absolute inset-0 size-full object-cover".