diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml
index bf8c967..ad2a99c 100644
--- a/.github/workflows/codspeed.yml
+++ b/.github/workflows/codspeed.yml
@@ -23,7 +23,7 @@ concurrency:
jobs:
benchmarks:
- runs-on: ubuntu-latest
+ runs-on: codspeed-macro
timeout-minutes: 30
permissions:
contents: read
diff --git a/packages/base-ui/src/components/checkbox/Checkbox.module.css b/packages/base-ui/src/components/checkbox/Checkbox.module.css
index ea71bc0..56d3e08 100644
--- a/packages/base-ui/src/components/checkbox/Checkbox.module.css
+++ b/packages/base-ui/src/components/checkbox/Checkbox.module.css
@@ -31,7 +31,8 @@
grid-template-columns: minmax(0, 1fr) var(--_ov-control-size);
}
-.Root[data-ov-label-position='start'] .Control {
+.Root[data-ov-label-position='start'] .Control,
+.Root[data-ov-label-position='start'] .ControlIndicator {
grid-column: 2;
}
@@ -168,24 +169,28 @@
box-shadow var(--ov-duration-interactive) var(--ov-ease-standard);
}
-.Root:not([data-disabled], [disabled]):hover .Control {
+.Root:not([data-disabled], [disabled]):hover .Control,
+.Root:not([data-disabled], [disabled]):hover .ControlIndicator {
background: var(--_ov-control-bg-hover);
border-color: var(--_ov-control-border-hover);
}
-.Root:is([data-checked], [data-indeterminate]) .Control {
+.Root:is([data-checked], [data-indeterminate]) .Control,
+.Root:is([data-checked], [data-indeterminate]) .ControlIndicator {
background: var(--_ov-control-bg-checked);
border-color: var(--_ov-control-border-checked);
color: var(--_ov-control-fg-checked);
}
-.Root:focus-visible .Control {
+.Root:focus-visible .Control,
+.Root:focus-visible .ControlIndicator {
border-color: var(--_ov-focus-border);
box-shadow: 0 0 0 1px var(--ov-color-state-focus-ring);
}
.Indicator {
display: inline-flex;
+ position: relative;
align-items: center;
justify-content: center;
inline-size: 100%;
@@ -202,22 +207,96 @@
transform: scale(1);
}
-.DefaultCheckIcon,
-.DefaultMinusIcon {
- inline-size: var(--_ov-indicator-size);
- block-size: var(--_ov-indicator-size);
+/* ── Fallback pseudo-elements for compound-part consumers using .Indicator with no children ── */
+
+.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;
+ transform: translate(-50%, -60%) rotate(45deg);
+}
+
+.Root[data-checked]:not([data-indeterminate]) .Indicator:empty::before {
+ display: block;
}
-.DefaultMinusIcon {
+.Indicator:empty::after {
+ content: '';
display: none;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 55%;
+ height: 2px;
+ background: currentColor;
+ border-radius: 1px;
+ transform: translate(-50%, -50%);
}
-.Root[data-indeterminate] .DefaultCheckIcon {
+.Root[data-indeterminate] .Indicator:empty::after {
+ display: block;
+}
+
+/* ── Combined Control + Indicator (flattened path: no custom indicator, keepMounted=true) ── */
+
+.ControlIndicator {
+ display: inline-flex;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+ inline-size: var(--_ov-control-size);
+ block-size: var(--_ov-control-size);
+ border-radius: var(--ov-size-choice-radius);
+ border: 1px solid var(--_ov-control-border);
+ background: var(--_ov-control-bg);
+ color: var(--_ov-control-fg);
+ transition:
+ background-color var(--ov-duration-interactive) var(--ov-ease-standard),
+ border-color var(--ov-duration-interactive) var(--ov-ease-standard),
+ color var(--ov-duration-interactive) var(--ov-ease-standard),
+ box-shadow var(--ov-duration-interactive) var(--ov-ease-standard);
+}
+
+/* ── Pseudo-element indicators on .ControlIndicator ── */
+
+.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;
+ transform: translate(-50%, -60%) rotate(45deg);
}
-.Root[data-indeterminate] .DefaultMinusIcon {
- display: inline;
+.Root[data-checked]:not([data-indeterminate]) .ControlIndicator::before {
+ display: block;
+}
+
+.ControlIndicator::after {
+ content: '';
+ display: none;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 55%;
+ height: 2px;
+ background: currentColor;
+ border-radius: 1px;
+ transform: translate(-50%, -50%);
+}
+
+.Root[data-indeterminate] .ControlIndicator::after {
+ display: block;
}
.Content {
diff --git a/packages/base-ui/src/components/checkbox/Checkbox.test.tsx b/packages/base-ui/src/components/checkbox/Checkbox.test.tsx
index 4155d12..7066f2d 100644
--- a/packages/base-ui/src/components/checkbox/Checkbox.test.tsx
+++ b/packages/base-ui/src/components/checkbox/Checkbox.test.tsx
@@ -35,6 +35,32 @@ describe('Checkbox', () => {
expect(screen.getByTestId('indicator')).toBeInTheDocument();
});
+ it('renders default checkbox without icon child elements', () => {
+ const { container } = renderWithTheme();
+ const checkbox = screen.getByRole('checkbox');
+ expect(checkbox).toHaveAttribute('data-checked');
+
+ // Flattened path: no LuCheck/LuMinus SVGs
+ const svgs = container.querySelectorAll('svg');
+ expect(svgs).toHaveLength(0);
+ });
+
+ it('falls back to nested structure when keepIndicatorMounted is false', () => {
+ renderWithTheme(
+
+ Nested path
+ ,
+ );
+ const checkbox = screen.getByRole('checkbox', { name: 'Nested path' });
+ expect(checkbox).toHaveAttribute('data-checked');
+ });
+
+ it('renders indeterminate state in flattened path', () => {
+ renderWithTheme();
+ const checkbox = screen.getByRole('checkbox');
+ expect(checkbox).toHaveAttribute('data-indeterminate');
+ });
+
it('applies label position and spread layout attributes', () => {
renderWithTheme(
diff --git a/packages/base-ui/src/components/checkbox/Checkbox.tsx b/packages/base-ui/src/components/checkbox/Checkbox.tsx
index ed298a1..201a9ad 100644
--- a/packages/base-ui/src/components/checkbox/Checkbox.tsx
+++ b/packages/base-ui/src/components/checkbox/Checkbox.tsx
@@ -6,7 +6,6 @@ import {
type HTMLAttributes,
type ReactNode,
} from 'react';
-import { LuCheck, LuMinus } from 'react-icons/lu';
import { cn, withBaseClassName } from '../../system/classnames';
import { styleDataAttributes } from '../../system/styleProps';
import type { StyledComponentProps } from '../../system/types';
@@ -98,6 +97,7 @@ const CheckboxItem = forwardRef, CheckboxIt
ref,
) {
const hasContent = Boolean(children) || Boolean(description);
+ const useFlattened = indicator === undefined && keepIndicatorMounted;
return (
, CheckboxIt
data-ov-layout={layout}
{...props}
>
-
-
- {indicator ?? (
- <>
-
-
- >
- )}
-
-
+ {useFlattened ? (
+
+ ) : (
+
+
+ {indicator}
+
+
+ )}
{hasContent ? (
{children ? {children} : null}
diff --git a/packages/benchmarks/src/base-ui/DataTable.bench.tsx b/packages/benchmarks/src/base-ui/DataTable.bench.tsx
index bd96c17..a3a3a87 100644
--- a/packages/benchmarks/src/base-ui/DataTable.bench.tsx
+++ b/packages/benchmarks/src/base-ui/DataTable.bench.tsx
@@ -50,8 +50,10 @@ function DataTableBench({ data }: { data: Row[] }) {
return (
-
-
+
+
+
+
);
}
diff --git a/packages/benchmarks/src/stubs/react-syntax-highlighter.ts b/packages/benchmarks/src/stubs/react-syntax-highlighter.ts
new file mode 100644
index 0000000..ab7ff57
--- /dev/null
+++ b/packages/benchmarks/src/stubs/react-syntax-highlighter.ts
@@ -0,0 +1,5 @@
+// Stub: react-syntax-highlighter is imported transitively via the base-ui
+// barrel (CodeBlock component) but no benchmark uses it. The real package
+// fails in CodSpeed's forked CJS process because refractor is ESM-only.
+export const Prism = () => null;
+export default () => null;
diff --git a/packages/benchmarks/vitest.config.ts b/packages/benchmarks/vitest.config.ts
index af34125..c20af22 100644
--- a/packages/benchmarks/vitest.config.ts
+++ b/packages/benchmarks/vitest.config.ts
@@ -43,10 +43,18 @@ export default defineConfig(async () => {
return {
plugins,
resolve: {
- alias: {
- '@omniview/base-ui': path.resolve(__dirname, '../base-ui/src/index.ts'),
- '@omniview/editors': path.resolve(__dirname, '../editors/src/index.ts'),
- },
+ alias: [
+ { find: '@omniview/base-ui', replacement: path.resolve(__dirname, '../base-ui/src/index.ts') },
+ { find: '@omniview/editors', replacement: path.resolve(__dirname, '../editors/src/index.ts') },
+ // Stub out react-syntax-highlighter and all sub-path imports (e.g.
+ // react-syntax-highlighter/dist/esm/styles/prism). Imported transitively
+ // via CodeBlock barrel but unused by benchmarks. The real package fails
+ // in CodSpeed's forked CJS process because refractor is ESM-only.
+ {
+ find: /^react-syntax-highlighter(\/.*)?$/,
+ replacement: path.resolve(__dirname, 'src/stubs/react-syntax-highlighter.ts'),
+ },
+ ],
},
test: {
benchmark: {