Skip to content

perf: checkbox optimization — CSS indicators and flattened tree#28

Merged
joshuapare merged 9 commits into
mainfrom
feat/checkbox-optimization
Mar 12, 2026
Merged

perf: checkbox optimization — CSS indicators and flattened tree#28
joshuapare merged 9 commits into
mainfrom
feat/checkbox-optimization

Conversation

@joshuapare
Copy link
Copy Markdown
Contributor

@joshuapare joshuapare commented Mar 12, 2026

Summary

  • Replace LuCheck/LuMinus React icon components with CSS pseudo-elements (::before for checkmark, ::after for minus) — eliminates 2 React component instances per checkbox
  • Flatten the CheckboxItem component tree when no custom indicator prop is passed — merges CheckboxControl span and CheckboxIndicator wrapper into a single BaseCheckbox.Indicator element with combined .ControlIndicator styles
  • Fix CodSpeed workflow to use codspeed-macro runners, enabling execution profiling (flame graphs) that were previously unavailable

Benchmark Results

Benchmark Before After Change
mount 960 hz (1.04ms) 1,540 hz (0.65ms) +60%
checked toggle 3,029 hz (0.33ms) 3,553 hz (0.28ms) +17%
mount 1000 2.43 hz (411ms) 4.01 hz (249ms) +65%

Design Details

  • Pseudo-elements scoped to .ControlIndicator (flattened path) and .Indicator:empty (compound-part fallback) — custom indicator prop content is never overlapped
  • Flattened path only activates when keepIndicatorMounted is true (the default) — falls back to nested structure otherwise
  • All existing props, variants, colors, sizes, and compound parts unchanged
  • Compound-part consumers (MarkdownPreview, AIMarkdown) get CSS indicators via .Indicator:empty fallback

Test plan

  • All 849 existing + new unit tests pass
  • Benchmarks confirm improvement across mount, toggle, and mount-1000
  • Visual verification in Storybook: checkmark/minus render correctly at sm/md/lg across all variants

Summary by CodeRabbit

  • Style

    • Enhanced checkbox visuals and layout: new combined control + indicator visuals for checked and indeterminate states, improved size/placement handling and hover/focus behavior.
  • Tests

    • Added tests covering default rendering, fallback indicator behavior, and indeterminate state.
  • Chores

    • Benchmarks environment updated; test runner aliases updated and a lightweight stub added for a syntax-highlighter dependency.
  • UI

    • Minor DataTable structure adjusted to wrap header/body inside a container.

…lass

Replace .DefaultCheckIcon/.DefaultMinusIcon icon rules with CSS
pseudo-elements (::before for checkmark, ::after for minus).
Add .ControlIndicator combined class merging Control + Indicator
styles for the flattened render path. Add .Indicator:empty fallback
for compound-part consumers.
Remove LuCheck/LuMinus imports. CheckboxItem now uses a flattened
tree (single BaseCheckbox.Indicator with .ControlIndicator class)
when no custom indicator prop is passed and keepIndicatorMounted
is true. Falls back to nested Control > Indicator structure for
custom indicators or keepIndicatorMounted=false.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 07a7496a-7a09-4428-b745-00f27a7ef67e

📥 Commits

Reviewing files that changed from the base of the PR and between 1756962 and 68fc1f1.

📒 Files selected for processing (1)
  • packages/benchmarks/src/base-ui/DataTable.bench.tsx

📝 Walkthrough

Walkthrough

CI benchmark runner switched to codspeed-macro. Checkbox component adds ControlIndicator styles and a flattened rendering path; tests for checkbox behaviors added. Benchmarks config stubs react-syntax-highlighter and updates Vitest aliasing; a DataTable benchmark JSX wrapper adjusted.

Changes

Cohort / File(s) Summary
CI Infrastructure
​.github/workflows/codspeed.yml
Benchmarks job runner changed from ubuntu-latest to codspeed-macro.
Checkbox Component
packages/base-ui/src/components/checkbox/Checkbox.tsx, packages/base-ui/src/components/checkbox/Checkbox.module.css, packages/base-ui/src/components/checkbox/Checkbox.test.tsx
Adds .ControlIndicator styling and pseudo-element indicators; introduces useFlattened flag to render a flattened Indicator path when appropriate; adjusts selectors for hover/focus/state; adds three tests covering default, keepIndicatorMounted fallback, and indeterminate rendering.
Benchmark Tooling & Stubs
packages/benchmarks/vitest.config.ts, packages/benchmarks/src/stubs/react-syntax-highlighter.ts, packages/benchmarks/src/base-ui/DataTable.bench.tsx
Converts Vitest resolve.alias to array form and adds a regex alias redirecting react-syntax-highlighter to a local stub; adds a minimal stub exporting Prism and a default no-op to avoid ESM dependency; wraps DataTable header/body in DataTable.Container in a benchmark file.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I tuned the checks with ears held high,
New ControlIndicators blink in the sky.
Benchmarks stubbed, the runner hops on cue,
Containers nest — a tidy view.
Happy hops for lines anew! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main optimization: replacing icon components with CSS indicators and flattening the component tree for performance.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/checkbox-optimization

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

react-syntax-highlighter imports refractor (ESM-only) via require()
in the forked CJS process, causing CI failures. Configure Vitest
deps optimizer to inline both packages.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/base-ui/src/components/checkbox/Checkbox.module.css`:
- Around line 220-221: Replace the capitalized CSS keyword occurrences of
"currentColor" with the lowercase "currentcolor" in the Checkbox styles;
specifically update the declarations using border-bottom and border-right (and
any other declarations in the same module where currentColor appears) inside
Checkbox.module.css so they use "currentcolor" to satisfy Stylelint's
value-keyword-case rule.

In `@packages/base-ui/src/components/checkbox/Checkbox.test.tsx`:
- Around line 58-62: The new test "renders indeterminate state in flattened
path" duplicates the existing "supports indeterminate state with default
indicator" for Checkbox.Item; either merge them or make this test assert a
distinguishing behavior: confirm the element still has data-indeterminate and
also assert the flattened-path variant renders without the SVG indicator (e.g.,
verify no SVG indicator is present or that label text is absent), or remove the
duplicate if behaviors are identical—update the test name to reflect the
differentiated assertion if you add one.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81a42fef-04bb-4c29-b293-42e9aca4ccf5

📥 Commits

Reviewing files that changed from the base of the PR and between ea3c565 and 2ae7797.

📒 Files selected for processing (5)
  • .github/workflows/codspeed.yml
  • packages/base-ui/src/components/checkbox/Checkbox.module.css
  • packages/base-ui/src/components/checkbox/Checkbox.test.tsx
  • packages/base-ui/src/components/checkbox/Checkbox.tsx
  • packages/benchmarks/vitest.config.ts

Comment on lines +220 to +221
border-bottom: 2px solid currentColor;
border-right: 2px solid currentColor;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use lowercase currentcolor for Stylelint compliance.

Stylelint reports value-keyword-case errors. While both currentColor and currentcolor are valid CSS, the project's linting configuration prefers lowercase.

🔧 Proposed fix
 .Indicator:empty::before {
   content: '';
   display: none;
   position: absolute;
   top: 50%;
   left: 50%;
   width: 35%;
   height: 60%;
-  border-bottom: 2px solid currentColor;
-  border-right: 2px solid currentColor;
+  border-bottom: 2px solid currentcolor;
+  border-right: 2px solid currentcolor;
   transform: translate(-50%, -60%) rotate(45deg);
 }
 .Indicator:empty::after {
   content: '';
   display: none;
   position: absolute;
   top: 50%;
   left: 50%;
   width: 55%;
   height: 2px;
-  background: currentColor;
+  background: currentcolor;
   border-radius: 1px;
   transform: translate(-50%, -50%);
 }
 .ControlIndicator::before {
   content: '';
   display: none;
   position: absolute;
   top: 50%;
   left: 50%;
   width: 35%;
   height: 60%;
-  border-bottom: 2px solid currentColor;
-  border-right: 2px solid currentColor;
+  border-bottom: 2px solid currentcolor;
+  border-right: 2px solid currentcolor;
   transform: translate(-50%, -60%) rotate(45deg);
 }
 .ControlIndicator::after {
   content: '';
   display: none;
   position: absolute;
   top: 50%;
   left: 50%;
   width: 55%;
   height: 2px;
-  background: currentColor;
+  background: currentcolor;
   border-radius: 1px;
   transform: translate(-50%, -50%);
 }

Also applies to: 237-237, 276-277, 293-293

🧰 Tools
🪛 Stylelint (17.4.0)

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

(value-keyword-case)


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

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/base-ui/src/components/checkbox/Checkbox.module.css` around lines
220 - 221, Replace the capitalized CSS keyword occurrences of "currentColor"
with the lowercase "currentcolor" in the Checkbox styles; specifically update
the declarations using border-bottom and border-right (and any other
declarations in the same module where currentColor appears) inside
Checkbox.module.css so they use "currentcolor" to satisfy Stylelint's
value-keyword-case rule.

Comment on lines +58 to +62
it('renders indeterminate state in flattened path', () => {
renderWithTheme(<Checkbox.Item indeterminate />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('data-indeterminate');
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider combining with existing indeterminate test or adding differentiating assertions.

This test is very similar to the existing test at lines 21-26 (supports indeterminate state with default indicator). The only difference is the absence of label text. Consider either:

  1. Adding an assertion that differentiates this test (e.g., verifying no SVGs like in the flattened path test)
  2. Combining with the existing test if the behavior is identical
💡 Suggested enhancement to differentiate the test
 it('renders indeterminate state in flattened path', () => {
-  renderWithTheme(<Checkbox.Item indeterminate />);
+  const { container } = renderWithTheme(<Checkbox.Item indeterminate />);
   const checkbox = screen.getByRole('checkbox');
   expect(checkbox).toHaveAttribute('data-indeterminate');
+  // Flattened path: no LuMinus SVGs, uses CSS pseudo-element
+  expect(container.querySelectorAll('svg')).toHaveLength(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
it('renders indeterminate state in flattened path', () => {
renderWithTheme(<Checkbox.Item indeterminate />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('data-indeterminate');
});
it('renders indeterminate state in flattened path', () => {
const { container } = renderWithTheme(<Checkbox.Item indeterminate />);
const checkbox = screen.getByRole('checkbox');
expect(checkbox).toHaveAttribute('data-indeterminate');
// Flattened path: no LuMinus SVGs, uses CSS pseudo-element
expect(container.querySelectorAll('svg')).toHaveLength(0);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/base-ui/src/components/checkbox/Checkbox.test.tsx` around lines 58 -
62, The new test "renders indeterminate state in flattened path" duplicates the
existing "supports indeterminate state with default indicator" for
Checkbox.Item; either merge them or make this test assert a distinguishing
behavior: confirm the element still has data-indeterminate and also assert the
flattened-path variant renders without the SVG indicator (e.g., verify no SVG
indicator is present or that label text is absent), or remove the duplicate if
behaviors are identical—update the test name to reflect the differentiated
assertion if you add one.

The deps.optimizer.web approach was insufficient on the CodSpeed
ARM64 runner. Switch to server.deps.inline which forces Vite to
transform react-syntax-highlighter and refractor before the forked
process attempts to require() them.
CodSpeed's forked process uses CJS require() which fails on
refractor (ESM-only), imported transitively via CodeBlock barrel.
No benchmark uses CodeBlock, so stub the module in setup.
Revert ineffective server.deps.inline approach.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/benchmarks/src/setup.ts`:
- Around line 4-12: Currently the global vi.mock calls for
'react-syntax-highlighter/dist/esm/prism-light' and
'react-syntax-highlighter/dist/cjs/prism-light' live in shared setup
(vi.mock(...)), which can silently hide future real usage; move these mocks out
of the shared setup and into a CodSpeed-specific test config or into only the
affected suites that need them (e.g., the CodSpeed runner test file or a
suite-level setup file). Locate the two vi.mock calls and either (a) wrap them
behind a CodSpeed-only conditional (check a dedicated env var like CODSPEED or a
test-runner flag) so they execute only for CodSpeed runs, or (b) remove them
from the global setup and add identical vi.mock calls to the specific test
suites/config that actually require stubbing the 'react-syntax-highlighter'
modules. Ensure the module names
('react-syntax-highlighter/dist/esm/prism-light' and
'react-syntax-highlighter/dist/cjs/prism-light') and the mock factory (default:
() => null) are preserved when relocating.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3684c85f-393f-4ab0-9ee3-29b40d3ad31a

📥 Commits

Reviewing files that changed from the base of the PR and between 11817fa and 9e280e4.

📒 Files selected for processing (1)
  • packages/benchmarks/src/setup.ts

Comment thread packages/benchmarks/src/setup.ts Outdated
Comment on lines +4 to +12
// react-syntax-highlighter imports refractor (ESM-only) via require().
// In CodSpeed's forked runner process the CJS require() of ESM fails.
// No benchmark uses CodeBlock, so stub the module out entirely.
vi.mock('react-syntax-highlighter/dist/esm/prism-light', () => ({
default: () => null,
}));
vi.mock('react-syntax-highlighter/dist/cjs/prism-light', () => ({
default: () => null,
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether any benchmark currently touches components that may depend on react-syntax-highlighter.
# Expected result if this global mock is safe today: no benchmark matches below.

rg -n -C2 --glob 'packages/benchmarks/**/*.{ts,tsx}' \
  'CodeBlock|MarkdownPreview|AIMarkdown|react-syntax-highlighter'

# Also locate app/component files that directly import the syntax highlighter,
# so you can compare them against benchmark imports.
rg -n -C2 --glob 'packages/**/*.{ts,tsx}' \
  'react-syntax-highlighter'

Repository: omniviewdev/ui

Length of output: 2453


Scope this mock to CodSpeed-only or to the affected suites (defensive best practice).

No current benchmark uses CodeBlock or react-syntax-highlighter, so the global mock in shared setup is safe today. However, if a future benchmark adds syntax-highlighting coverage, the mock would silently suppress it rather than fail loudly. To guard against this, move the mock into a CodSpeed-specific config or only mock it in suites that actually need it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/benchmarks/src/setup.ts` around lines 4 - 12, Currently the global
vi.mock calls for 'react-syntax-highlighter/dist/esm/prism-light' and
'react-syntax-highlighter/dist/cjs/prism-light' live in shared setup
(vi.mock(...)), which can silently hide future real usage; move these mocks out
of the shared setup and into a CodSpeed-specific test config or into only the
affected suites that need them (e.g., the CodSpeed runner test file or a
suite-level setup file). Locate the two vi.mock calls and either (a) wrap them
behind a CodSpeed-only conditional (check a dedicated env var like CODSPEED or a
test-runner flag) so they execute only for CodSpeed runs, or (b) remove them
from the global setup and add identical vi.mock calls to the specific test
suites/config that actually require stubbing the 'react-syntax-highlighter'
modules. Ensure the module names
('react-syntax-highlighter/dist/esm/prism-light' and
'react-syntax-highlighter/dist/cjs/prism-light') and the mock factory (default:
() => null) are preserved when relocating.

Previous approaches (deps.optimizer, server.deps.inline, vi.mock)
failed because CodSpeed's forked process loads modules via Node's
require() before Vitest transforms apply. A Vite resolve alias
redirects all react-syntax-highlighter imports (including sub-paths
like dist/esm/styles/prism) to a stub before module resolution.
DataTable.Header and DataTable.Body were rendered directly inside the
Root div, causing <thead>/<tbody> to appear outside a <table>. Adding
DataTable.Container provides the required <table> wrapper element.
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 12, 2026

Merging this PR will degrade performance by 86.83%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 12 regressed benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime mount 1.8 ms 5.4 ms -65.99%
WallTime mount 100 rows 41.7 ms 218.6 ms -80.94%
WallTime mount section 393.4 µs 1,882.7 µs -79.11%
WallTime checked toggle 1.2 ms 4.8 ms -74.29%
WallTime mount 1000 810.3 ms 1,964.1 ms -58.75%
WallTime variant change 213.5 µs 1,621.9 µs -86.83%
WallTime mount div 434.9 µs 2,018.2 µs -78.45%
WallTime mount 1000 35.3 ms 192.8 ms -81.68%
WallTime mount 1000 98.1 ms 442.9 ms -77.84%
WallTime mount 1000 rows 436.4 ms 2,060.9 ms -78.83%
WallTime mount 560.7 µs 2,718.6 µs -79.38%
WallTime mount with decorators 614.5 µs 3,265.6 µs -81.18%

Comparing feat/checkbox-optimization (68fc1f1) with main (ea3c565)

Open in CodSpeed

@joshuapare joshuapare merged commit d0c6abe into main Mar 12, 2026
2 of 3 checks passed
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.

1 participant