diff --git a/.claude/skills/component/references/anti-patterns.md b/.claude/skills/component/references/anti-patterns.md
index 5878d13e..8cce8ec6 100644
--- a/.claude/skills/component/references/anti-patterns.md
+++ b/.claude/skills/component/references/anti-patterns.md
@@ -65,6 +65,42 @@ import { Button } from '@lib/button';
---
+## Unnecessary Render Function Wrappers
+
+```tsx
+// BAD: Function wrapper just passes props through
+ (
+
+ )}
+/>
+
+// BAD: Trivial passthrough
+} />
+
+// BAD: Passing component reference directly — breaks hooks reconciliation
+
+
+// GOOD: Element form — className and children on the headless component
+}>
+
+
+
+// GOOD: Element form for passthrough
+} />
+
+// GOOD: Element form with extra props on render target
+}>
+
+
+```
+
+**Why it fails:** Function wrappers add indirection, noise, and implicit `any` on `props` parameter. Component references (`render={Component}`) bypass React's reconciliation — components are called as plain functions, breaking hooks. Use element form (`render={}`) or function form only when rendering a different element type or accessing component state.
+
+---
+
## The `as` Prop
```tsx
@@ -273,6 +309,7 @@ const Button = forwardRef(({ children, ...props
- [ ] State styled via data attributes, not inline
- [ ] No CSS shipped in component package
- [ ] Polymorphism via `render` or `asChild`, not `as`
+- [ ] No unnecessary render function wrappers (`render={}` over `render={(props) => }`)
- [ ] Both controlled and uncontrolled modes
- [ ] Nested instances don't interfere
- [ ] Exit animations possible
diff --git a/.claude/skills/component/references/polymorphism.md b/.claude/skills/component/references/polymorphism.md
index 00c4908e..3465ed12 100644
--- a/.claude/skills/component/references/polymorphism.md
+++ b/.claude/skills/component/references/polymorphism.md
@@ -17,22 +17,53 @@ Polymorphism allows users to customize which element a component renders as. Two
## `render` Pattern (Preferred)
+Two forms: element and function.
+
### Element Form — Simple Cases
```tsx
-// Renders MyButton with Dialog.Trigger behavior
-}>Open dialog
+// Renders Button with PlayButton behavior — clones element, merges props
+}>
+
+
+
+// Pass props to the render target directly
+}>
+
+
```
-### Function Form — State Access
+The headless component clones the element and merges its own props onto it.
+
+### Function Form — State Access or Different Element
```tsx
// Access internal state for conditional rendering
{state.checked ? : }}
/>
+
+// Render a fundamentally different element type
+ (
+
+
+
+ )}
+/>
```
+### When to Use Which
+
+| Scenario | Form |
+| --- | --- |
+| Simple element swap | `render={}` |
+| Render target needs its own props | `render={}` |
+| Need internal state access | `render={(props, state) => ...}` |
+| Rendering a different element type | `render={(props) =>
...}` |
+
+**Do not** pass component references directly (`render={Component}`). React calls render functions as plain functions, which breaks hooks reconciliation. Always use element form (`render={}`) or function form.
+
---
## `asChild` Pattern
@@ -51,7 +82,7 @@ Polymorphism allows users to customize which element a component renders as. Two
| Concern | `render` | `asChild` |
| ------------------ | ------------------------------------- | ----------------------------------------- |
-| **Prop flow** | Explicit — you spread props visibly | Hidden — `cloneElement` merges implicitly |
+| **Prop flow** | Explicit — element or function forms | Hidden — `cloneElement` merges implicitly |
| **State access** | Function form exposes component state | No state access |
| **TypeScript** | Predictable inference | Can slow IDE autocomplete |
| **Debugging** | Traceable prop flow | Magic makes tracing difficult |
diff --git a/.claude/skills/component/references/react.md b/.claude/skills/component/references/react.md
index 79a6769a..83409612 100644
--- a/.claude/skills/component/references/react.md
+++ b/.claude/skills/component/references/react.md
@@ -157,6 +157,7 @@ useImperativeHandle(actionsRef, () => ({ open, close }));
- Accept `ReactElement` or `(props, state) => ReactElement`
- Use `cloneElement` for element form
- Use `mergeProps` to combine internal + external props
+- **Do not** accept component references (`render={Component}`) — calling components as plain functions breaks hooks reconciliation
**`mergeProps` behavior:**
diff --git a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx
index eaa77156..9dfb04a6 100644
--- a/packages/react/src/presets/audio/minimal-skin.tailwind.tsx
+++ b/packages/react/src/presets/audio/minimal-skin.tailwind.tsx
@@ -78,6 +78,10 @@ const SliderFill = forwardRef & { type?: '
);
});
+const SliderBuffer = forwardRef>(function SliderBuffer(props, ref) {
+ return ;
+});
+
const SliderThumb = forwardRef & { persistent?: boolean }>(function SliderThumb(
{ persistent, className, ...props },
ref
@@ -102,15 +106,11 @@ function VolumePopover(): ReactNode {
const volumeUnsupported = usePlayer((s) => s.volumeAvailability === 'unsupported');
const muteButton = (
- (
-
- )}
- />
+ }>
+
+
+
+
);
if (volumeUnsupported) return muteButton;
@@ -119,9 +119,9 @@ function VolumePopover(): ReactNode {
- }>
- }>
- } />
+ }>
+ }>
+ } />
} />
@@ -145,15 +145,11 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
(
-
- )}
- />
+ }>
+
+
+
+
}
/>
@@ -164,17 +160,12 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
(
-
- )}
- />
+ }>
+
+
+ {SEEK_TIME}
+
+
}
/>
Seek backward {SEEK_TIME} seconds
@@ -183,17 +174,12 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
(
-
- )}
- />
+ }>
+
+
+ {SEEK_TIME}
+
+
}
/>
Seek forward {SEEK_TIME} seconds
@@ -207,23 +193,19 @@ export function MinimalAudioSkinTailwind(props: MinimalAudioSkinProps): ReactNod
- }>
- }>
- } />
- } />
+ }>
+ }>
+ } />
+ } />
- } />
+ } />