[Feature] AspectRatio: match shadcn (Base UI + vega)
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.




Changes
- AspectRatio renders one
<div data-slot="aspect-ratio">withstyle="--ratio: N"andrelative aspect-(--ratio), the CSSaspect-ratioproperty. Before, it rendered a wrapper with apadding-bottompercentage and an innerabsolute inset-0div. - Caller attrs (
class,id,data,style) now land on that div. Before, they went to the inner div, so classes such asmax-w-*,mx-autooroverflow-hiddenacted 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:deprecatedwarning). Invalid ratios raiseArgumentError. style:as a String or a Hash is merged with--ratio.- Content taller than the ratio grows the box, as in shadcn. Add
overflow-hiddento 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-mutedbackground. Passclass: "bg-muted"to keep it. - No
[&>img]styles. A direct<img>child no longer fills the box on its own; give itclass: "absolute inset-0 size-full object-cover". - Content taller than the ratio now grows the box instead of overflowing it.
Testing instructions
cd gem && bundle exec rakecd audit && bundle exec rakecd docs && bin/dev, then open/docs/aspect_ratio,/docs/cardand/docs/dialog. The images fill their boxes at the right ratio, and the "Content taller than the ratio" example clips its paragraph.- 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-mutedbackground; passclass: "bg-muted"to keep it. - A direct
<img>child no longer fills the box; give itclass: "absolute inset-0 size-full object-cover".