[Feature] Codeblock: match shadcn (Base UI + vega)
CI
1d ago+507+507+012345678901234567890123456789-44-44-01234567890123456789
The shadcn audit of the Codeblock found one visual gap and one behavior difference. This PR fixes both and widens the audit fixture so every part of the component gets checked in the browser.
What changes
CodeblockContentgets shadcn'sscroll-fade-x scrollbar-thin data-wrap:scroll-fade-y. When a line is wider than the block, the cut edge fades out (and the start fades too once you scroll), and the scrollbar is thin. Withwrap: truethe fade runs top and bottom.- The
scroll-fade-xandscroll-fade-yutilities are ported from shadcn'stailwind.css(MIT) into the installer template (tailwind.css.erb) and the docs CSS, next to thescroll-fade-bMessageScroller already uses.scrollbar-thincomes with Tailwind 4.3. - A failed copy shows nothing, as in shadcn. Before, the button and the live region said "Copy failed" for 2s.
Deprecated
Codeblock(clipboard_error:)andCodeblockCopyButton(error_label:)are still accepted but ignored, with a deprecation warning. They can be removed in the next major.
Audit
- The fixture now renders three blocks: the floating copy button, a header (title and copy button) with a footer, and numbered lines using the
removed,addedandhighlightedvariants. The map gains selectors forCodeblockHeader,CodeblockTitle,CodeblockLineandCodeblockFooter, which the audit never rendered before. - The map lists
styleonCodeblockContentandonClickonCodeblockCopyButton.onClickis a registered divergence (audit/divergences/codeblock.yml): Ruby has no React event prop, and adata: {action: ...}passed in runs next to the copy. - Baseline moved to shadcn
8a353667. - One tooling gap, left for another PR:
collectdoesn't comparemask-imageorscrollbar-width, so onmainit reported the content as matching. I checked both by hand here.
Before / after
A line wider than the block, light mode, pointer over the block (the copy button only shows on hover or focus):

Audit on main (before)
shadcn audit ruby-ui/ruby_ui
base 1fd93db0 head 1fd93db0 shadcn 8a353667
⚠️ 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)
✅ Theme neutral tokens match shadcn
Codeblock (ref: bases/base/ui/code-block.tsx + style-vega.css @ 8a353667)
❌ Look CodeblockContent: no scroll-fade-x / data-wrap:scroll-fade-y / scrollbar-thin (mask-image: none, scrollbar-width: auto)
vega: scroll-fade-x scrollbar-thin data-wrap:scroll-fade-y (mask-image linear-gradient fading the overflowing edge up to min(12%, 40px), scrollbar-width: thin)
Override of collect's 'CodeblockContent: computed styles match': collect doesn't compare mask-image or scrollbar-width. Verified with getComputedStyle at 1280x800 and by injecting an overflowing line on both sides (screens/codeblock-overflow-*): shadcn fades the cut-off end of the line, RubyUI cuts it hard. The utilities come from shadcn's tailwind.css (@utility scroll-fade-x/-y); RubyUI's install template ports only scroll-fade-b. Other computed properties match. Requested: add scroll-fade-x scrollbar-thin data-wrap:scroll-fade-y to CodeblockContent and port @utility scroll-fade-x/-y (+ scroll-fade-reveal-* keyframes) to the installer tailwind.css.erb and the docs CSS. Requested: run `baseline codeblock 8a3536678a528cc012b9e62484290e79d0be410f` in this PR.
↳ bases/base/ui/code-block.tsx:169
⏸ Look CodeblockHeader: no selector in the component map. Static compare: RubyUI class string equals reference effective classes (style-vega.css:1705); not rendered by the fixture. Requested: extend audit/fixtures/codeblock (header with title + actions, footer, line_numbers, added/removed lines) on both sides and add selectors for these parts to audit/component_map/codeblock.yml.
⏸ Look CodeblockTitle: no selector in the component map. Static compare: class string equals reference effective classes (style-vega.css:1713); not rendered by the fixture. Requested: extend audit/fixtures/codeblock (header with title + actions, footer, line_numbers, added/removed lines) on both sides and add selectors for these parts to audit/component_map/codeblock.yml.
⏸ Look CodeblockLine: no selector in the component map. Static compare: base classes and added/removed/highlighted variants equal the reference (style-vega.css:1728-1741); not rendered by the fixture. Requested: extend audit/fixtures/codeblock (header with title + actions, footer, line_numbers, added/removed lines) on both sides and add selectors for these parts to audit/component_map/codeblock.yml.
⏸ Look CodeblockFooter: no selector in the component map. Static compare: class string equals reference effective classes (style-vega.css:1709); not rendered by the fixture. Requested: extend audit/fixtures/codeblock (header with title + actions, footer, line_numbers, added/removed lines) on both sides and add selectors for these parts to audit/component_map/codeblock.yml.
✅ Look CodeblockActions, CodeblockCopyButton (hovered): absolute top/end 12px, 24x24, bg-background, ring-1 ring-foreground/10, radius 8px, 12px icon
Override of collect's 'no visible element in any reached state': collect only reaches closed (actions sit at opacity 0 until hover/focus). Driven by hand with hover; computed styles match. Copy button: shadcn size-6 vs RubyUI h-6 w-6, same 24px. Settles the reference warning (copy button rendered through <Button>).
↳ sandbox DOM + getComputedStyle (hover state, by hand)
⏸ Look properties: font-family
Codeblock — explained by environment diff: font Kept as is: environment diff, not a Codeblock finding.
✅ ARIA CodeblockActions, CodeblockCopyButton: button "Copy" (type=button) + aria-live=polite sr-only region; "Copied" in both after a copy, reset after 2s
Override of collect's 'no visible element' lines for both parts: verified by snapshot and by reading aria-label/live region before and after the click. shadcn's button also has tabindex=0 (Base UI), RubyUI relies on the native button; same tab order.
↳ accessibility tree (by hand, hover + click)
✅ ARIA Codeblock, CodeblockContent: accessibility tree matches
↳ accessibility tree (shadcn-audit collect)
✅ Props variant, size (Codeblock); wrap, line_numbers, start_line (CodeblockContent); value, variant, size (CodeblockCopyButton); variant (CodeblockLine)
rubyui_only: syntax (Rouge), clipboard / clipboard_success / clipboard_error and the content kwargs on the Codeblock shorthand, label / copied_label / error_label on CodeblockCopyButton. value is optional in RubyUI (falls back to the block's code); required in shadcn.
↳ bases/base/ui/code-block.tsx:25-31,147-159,85-92,208-213
⏸ Props Map gap: the .tsx declares style (CodeblockContent) and onClick (CodeblockCopyButton), absent from audit/component_map/codeblock.yml. RubyUI takes style: (merged after counter-reset, caller wins, as shadcn) and onClick has no Ruby counterpart (data-action passes through attrs). Requested: add style: style to CodeblockContent props in audit/component_map/codeblock.yml and record onClick as having no Ruby counterpart (data-action via attrs).
✅ Behavior Copy writes the code to the clipboard, icon copy→check, aria-label + live region "Copied", reset after 2s; actions revealed on hover and focus-within; Tab: copy button → content (<pre tabindex=0>), content focus shows ring-3 ring-ring/50 + border-ring on the root
Light theme, 1280x800.
↳ bases/base/ui/code-block.tsx:93-145, browser run (clipboard.writeText recorded on both sides)
⚠️ Behavior writeText rejects (or no Clipboard API) → aria-label and live region "Copy failed" for 2s
shadcn: writeText rejects → nothing (.catch(() => {})), label stays "Copy"
Verified in the browser by making writeText reject on both sides. RubyUI's behavior is the more useful one (failure announced to screen readers); deliberate extra, not registered as a divergence. Requested: align with shadcn — swallow copy failures (no error label or announcement). Removing clipboard_error:/error_label: is a public API change; deprecate or note it in the changelog.
↳ bases/base/ui/code-block.tsx:113-119
✅ Animation actions: transition-property opacity, 150ms, opacity 0 → 1 on hover/focus-within, always on pointer:coarse
Computed transition on both sides.
↳ bases/base/ui/code-block.tsx:77
✅ Native Stimulus controller only calls navigator.clipboard.writeText; scrolling via native overflow + tabindex=0 on <pre> — native
No hand-rolled behavior with a native replacement in native-html.md.
↳ https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
✅ Spec scrollable <pre> reachable by keyboard (tabindex=0); copy result announced through aria-live=polite
No spec violation observed on shadcn's side.
↳ https://www.w3.org/TR/WCAG22/#keyboard
Audit on this PR (after)
shadcn audit ruby-ui/ruby_ui
base 1fd93db0 head 67fc7e8e shadcn 8a353667
⚠️ 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)
✅ Theme neutral tokens match shadcn
Codeblock (ref: bases/base/ui/code-block.tsx + style-vega.css @ 8a353667)
⏸ Look properties: font-family
Codeblock, CodeblockHeader, CodeblockTitle, CodeblockActions, CodeblockCopyButton, CodeblockFooter — explained by environment diff: font
✅ Look CodeblockContent, CodeblockLine: computed styles match
Rendered preview: 162a9832 (67fc7e8e only adds audit/divergences/codeblock.yml). Fixture now covers header, title, footer and numbered added/removed/highlighted lines. mask-image and scrollbar-width (not in collect's property list) checked by hand: identical gradient strings at scrollLeft 0 and 150 with an overflowing line (screens/codeblock-overflow-*-now.png), scrollbar-width: thin on both.
↳ computed styles (shadcn-audit collect)
✅ ARIA Codeblock, CodeblockHeader, CodeblockTitle, CodeblockActions, CodeblockCopyButton, CodeblockContent, CodeblockLine, CodeblockFooter: accessibility tree matches
↳ accessibility tree (shadcn-audit collect)
✅ Look CodeblockActions, CodeblockCopyButton (hovered): absolute top/end 12px, 24x24, bg-background, ring-1 ring-foreground/10, radius 8px, 12px icon
Floating actions sit at opacity 0 until hover/focus, so collect sees them only inside the header; the floating state was driven by hand. Unchanged by this PR.
↳ sandbox DOM + getComputedStyle (hover state, by hand)
✅ Props variant, size (Codeblock); style, wrap, line_numbers, start_line (CodeblockContent); value, variant, size (CodeblockCopyButton); variant (CodeblockLine)
rubyui_only: syntax, clipboard, clipboard_success, label, copied_label; clipboard_error / error_label are deprecated and ignored.
↳ bases/base/ui/code-block.tsx:25-31,147-159,85-92,208-213
✅ Props no onClick on CodeblockCopyButton; a data-action passed through attrs runs alongside the copy — intentional
Server-rendered component: there is no React event prop. Extra behavior hooks in through Stimulus with data: {action: "click->my-controller#method"}, merged with the button's own copy action.
↳ audit/divergences/codeblock.yml
✅ Behavior Copy writes the code, icon copy→check, aria-label + live region "Copied", reset after 2s; a failed copy (writeText rejects or no Clipboard API) shows nothing; actions revealed on hover and focus-within
Success and failure driven in the browser on both sides (writeText stubbed to resolve / reject).
↳ bases/base/ui/code-block.tsx:93-145, browser run
✅ Animation actions: transition-property opacity, 150ms; content scroll fade driven by scroll-timeline animations scroll-fade-reveal-s/-e
Computed on both sides.
↳ bases/base/ui/code-block.tsx:77,169
✅ Native Stimulus controller only calls navigator.clipboard.writeText; scroll fade is CSS scroll-driven animation; scrolling via native overflow + tabindex=0 on <pre> — native
No hand-rolled behavior with a native replacement in native-html.md.
↳ https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
✅ Spec scrollable <pre> reachable by keyboard (tabindex=0); copy result announced through aria-live=polite
No spec violation observed on shadcn's side.
↳ https://www.w3.org/TR/WCAG22/#keyboard
Test steps
cd gem && bundle exec rake(tests and standardrb).cd docs && bin/dev, open/docs/codeblockand narrow the window until a line overflows. The cut edge fades, and scrolling right fades the start as well.- Click a copy button on localhost: the check and "Copied" show for 2s. Opened over plain http from another machine (no Clipboard API), the click changes nothing.
- With
Warning[:deprecated] = true,Codeblock(code, clipboard_error: "x")prints a deprecation warning.
🤖 Generated with Claude Code
Summary by cubic
Closes the last visual and behavioral gaps between the Codeblock and shadcn's: overflowing lines now fade at the cut edge instead of being clipped, and a failed copy shows nothing instead of announcing "Copy failed".
Migration
clipboard_error:anderror_label:are ignored and emit a deprecation warning; remove them in the next major.- The new
scroll-fade-x/scroll-fade-yutilities were added to the installer template and the manual install docs CSS, so existing projects must regenerate their Tailwind CSS or copy the utilities to get the fade.
Audit
- The fixture now renders a header, title, footer, and numbered diff lines; the map gains selectors for
CodeblockHeader,CodeblockTitle,CodeblockLine, andCodeblockFooter. - Registered
onClickas a divergence: a Rubydata: {action: ...}runs alongside the copy.