port-base-ui
Automates porting of headless UI components between React and Blazor.
Install
mkdir -p .claude/skills/port-base-ui && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/17393" && unzip -o skill.zip -d .claude/skills/port-base-ui && rm skill.zipInstalls to .claude/skills/port-base-ui
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Ports a complete headless UI component from Base UI (React) to BlazeUI.Headless (Blazor). Use when the user asks to "port [Component] from Base UI", "implement [Component] headless", "add [Component] to BlazeUI.Headless", or wants to bring an entire Base UI component (with all its sub-parts) into the Blazor headless library. Covers fetching Base UI source and tests from GitHub, planning the Blazor file structure, implementing all sub-parts, writing bUnit tests, and verifying the build. Do NOT use for styled/template components (use port-component instead), fixing existing components, adding individual sub-parts to an already-ported component, or general code review.Key capabilities
- →Fetch component directory listing from GitHub
- →Fetch implementation files for sub-parts
- →Extract default HTML element tags and ARIA attributes
- →Identify data attributes from `*DataAttributes.ts` and state shape
- →Fetch and read Base UI tests to identify high-value tests
- →Implement the equivalent headless component in Blazor with bUnit tests
How it works
The skill ports a headless UI component from Base UI (React) to BlazeUI.Headless (Blazor) by fetching and analyzing the React source and tests, then implementing the Blazor equivalent with unit tests.
Inputs & outputs
When to use port-base-ui
- →Port component to Blazor
- →Add headless component
- →Migrate Base UI to BlazeUI
About this skill
Port Base UI Component
Ports a complete headless component from Base UI's React source to BlazeUI.Headless in Blazor.
Critical: Base UI Is the Source of Truth
Every implementation decision must be grounded in the actual Base UI source code. Do not guess at behavior, invent ARIA attributes, or assume DOM structure. Fetch and read the React implementation and tests before writing any Blazor code. When in doubt, re-read the Base UI source.
Workflow
Step 1: Fetch and read the Base UI source
Fetch the component directory listing from GitHub to discover all sub-parts:
WebFetch https://api.github.com/repos/mui/base-ui/contents/packages/react/src/{component-name}
Then fetch the implementation file for every sub-part directory found. The typical structure is:
packages/react/src/{component-name}/
root/ — Root state container
trigger/ — Interactive trigger element
popup/ — Floating popup content
positioner/ — Floating-ui positioning wrapper
portal/ — React portal
backdrop/ — Overlay backdrop
arrow/ — Popup arrow element
... — Component-specific sub-parts
index.parts.ts — Canonical list of all exported sub-parts
For each sub-part, fetch and read the main .tsx file:
WebFetch https://raw.githubusercontent.com/mui/base-ui/master/packages/react/src/{component-name}/{sub-part}/{PascalName}.tsx
Also fetch any context files (*Context.ts), data attribute enums (*DataAttributes.ts), and the root's hook file if it contains core logic.
Critical: Understanding Base UI's default state-to-data-attribute mapping
Base UI's getStateAttributesProps (in packages/react/src/utils/getStateAttributesProps.ts) has an implicit default mapping: for any state property that is truthy, it automatically emits data-{key}="". For example, if a component's state is { disabled: true, open: false }, the rendered element gets data-disabled="" (truthy) but no data-open (falsy). This happens even when no explicit *DataAttributes.ts file exists for that component.
When auditing data attributes:
- Check the
*DataAttributes.tsfile if it exists — it may define additional custom-mapped attributes - Also check the state shape (
*Stateinterface) — every truthy state field produces adata-*attribute via the default mapping - Check for
stateAttributesMappingoverrides in the component'suseRenderElementcall — custom mappings (likefieldValidityMapping,popupStateMapping,transitionStatusMapping) replace the default for specific fields - The absence of a
*DataAttributes.tsfile does not mean "no data attributes" — it means the component uses only the default mapping from its state shape
Native HTML attributes (like disabled on <fieldset> or <button>) pass through via ...elementProps spread in useRenderElement and are not controlled by the state-to-data-attribute mapping.
Extract from each sub-part:
- Default HTML element tag (what
useRenderElementrenders) - ARIA attributes (roles,
aria-expanded,aria-controls,aria-labelledby, etc.) - Data attributes (from
*DataAttributes.tsand the default state mapping — check both) - State shape (what the component's state object contains — every truthy field becomes a
data-*attribute) - Context dependencies (what it reads from parent contexts)
- Event handling (click, keyboard, hover-intent, pointer events)
- JS-heavy behavior (positioning, pointer lock, scroll detection, focus trapping)
Step 2: Fetch and read the Base UI tests
Look for test files in the component directory and in the shared test directory:
WebFetch https://api.github.com/repos/mui/base-ui/contents/packages/react/src/{component-name}/root
Test files are named *.test.tsx and live alongside the sub-part implementation. Also check:
WebFetch https://api.github.com/repos/mui/base-ui/contents/packages/react/test
From each test file, identify high-value tests to port:
- Port: ARIA contract tests, keyboard navigation, state transitions, disabled/readonly behavior, accessibility assertions
- Skip: React-specific tests (ref forwarding, hook internals, render prop composition,
React.forwardRefbehavior)
Step 3: Read existing BlazeUI patterns
Before writing code, read the codebase to understand established patterns:
- Read CLAUDE.md and SESSION.md for architectural context and known issues
- Read an analogous existing component — pick one that matches the new component's category:
- Overlay with popup: read
DialogorPopovercomponents - Inline interactive: read
ToggleorCheckboxcomponents - Container with items: read
AccordionorTabscomponents - Form control: read
NumberFieldorSelectcomponents
- Overlay with popup: read
- Read the base classes you'll extend:
src/BlazeUI.Headless/Core/BlazeElement.csfor DOM-rendering componentssrc/BlazeUI.Headless/Core/OverlayRoot.csfor overlay root componentssrc/BlazeUI.Headless/Core/ComponentState.csfor controlled/uncontrolled state
- Read existing test files in
test/BlazeUI.Headless.Tests/Components/for test patterns
Step 4: Plan the Blazor file structure
Before coding, produce a plan with one file per line. Map every Base UI sub-part to a BlazeUI file:
src/BlazeUI.Headless/Components/{Component}/
{Component}Root.cs — extends OverlayRoot or ComponentBase
{Component}Context.cs — internal sealed class, cascaded to children
{Component}Trigger.cs — extends BlazeElement, renders button
{Component}Popup.cs — extends BlazeElement, mounted/unmounted
...
src/BlazeUI.Headless/wwwroot/js/{component}/
{component}.js — JS module (only if component needs JS interop)
test/BlazeUI.Headless.Tests/Components/{Component}/
{Component}Tests.cs — bUnit tests
For each file, note:
- Which Base UI file it corresponds to
- What base class it extends
- Key ARIA attributes it must emit
- Whether it needs JS interop
Share this plan with the user before proceeding.
Step 5: Implement the context class
Create the internal context class first — every other component depends on it.
Follow this pattern exactly:
using Microsoft.JSInterop;
namespace BlazeUI.Headless.Components.{Component};
internal sealed class {Component}Context
{
// Open/active state
public bool Open { get; set; }
// Element IDs for ARIA wiring
public string TriggerId { get; set; } = "";
public string PopupId { get; set; } = "";
// Delegates back to Root
public Func<bool, Task> SetOpen { get; set; } = _ => Task.CompletedTask;
// JS module (shared with children that need interop)
public IJSObjectReference? JsModule { get; set; }
public DotNetObjectReference<{Component}Root>? DotNetRef { get; set; }
}
Only add properties that the Base UI context actually provides. Do not invent properties.
Step 6: Implement the root component
The root manages state, owns the JS module lifecycle, and cascades context. Choose the correct base class:
| Component type | Base class | Examples |
|---|---|---|
| Overlay with popup | OverlayRoot | Dialog, Menu, Popover, Select, Tooltip |
| Inline interactive | BlazeElement | Toggle, Switch, Checkbox, NumberField |
| State-only container | ComponentBase | Accordion, Tabs, NavigationMenu |
For OverlayRoot subclasses, implement the abstract contract:
ModulePath— path to JS moduleJsInstanceKey— unique instance key for JSSyncContextState()— push open state onto contextOnJsInitializedAsync()— create DotNetRef, push JS refs onto contextOnDispose()— dispose DotNetRefOnBeforeCloseAsync()— enqueue hide mutation (if applicable)[JSInvokable]callback methods matching JS event handlers
For the constructor, generate element IDs via IdGenerator.Next(...) and assign delegate wiring.
Step 7: Implement child components
Each child component is a BlazeElement subclass that:
- Receives context via
[CascadingParameter] internal {Component}Context Context { get; set; } = default!; - Defines
DefaultTagmatching the Base UI default element - Returns a state record from
GetCurrentState() - Yields data attributes from
GetDataAttributes()— match Base UI's default state mapping and any*DataAttributes.tsoverrides (see "Understanding Base UI's default state-to-data-attribute mapping" above) - Yields ARIA and event attributes from
GetExtraAttributes()
State records are readonly record struct with the minimum fields needed:
public readonly record struct {Component}TriggerState(bool Open);
Step 8: Implement JS module (if needed)
Only create a JS module if the component requires browser APIs that Blazor cannot access (positioning, pointer lock, scroll detection, focus trapping, hover-intent, keyboard navigation).
Place it at src/BlazeUI.Headless/wwwroot/js/{component}/{component}.js.
Follow the established pattern:
- Import utilities from
/_content/BlazeUI.Headless/blazeui.core.js - Use a
Mapkeyed by instance ID for per-instance state - Export
init(), action functions, anddispose(instanceKey) - Handle cleanup in
dispose()(remove listeners, clear timers, delete from Map)
Step 9: Write bUnit tests
Create test/BlazeUI.Headless.Tests/Components/{Component}/{Component}Tests.cs.
Port high-value tests from Base UI's test files. Translate React Testing Library patterns to bUnit:
| React Testing Library | bUnit equivalent |
|---|---|
screen.getByRole('button') | cut.Find("button") or cut.Find("[role='button']") |
expect(el).toHaveAttribute('aria-expanded', 'true') | Assert.Equal("true", el.GetAttribute("aria-expanded")) |
fireEvent.click(el) | el.Click() |
expect(el).toBeInTheDocument() | Assert.NotNull(cut.Find(...)) |
| `expect(el).not.toBeInTheDocum |
Content truncated.
When not to use it
- →For styled/template components
- →For fixing existing components
- →For adding individual sub-parts to an already-ported component
Limitations
- →Every implementation decision must be grounded in the actual Base UI source code
- →The skill does not port React-specific tests (ref forwarding, hook internals, render prop composition)
- →The skill does not handle styled/template components
How it compares
This skill provides a structured, source-grounded approach to porting UI components between different frameworks, ensuring fidelity to the original behavior and attributes, unlike a manual port that might miss subtle implementation details.
Compared to similar skills
port-base-ui side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| port-base-ui (this skill) | 0 | 4mo | Review | Advanced |
| design-system-enforcer | 0 | 1mo | No flags | Intermediate |
| add-feature | 0 | 3mo | Review | Advanced |
| react-modernization | 21 | 2mo | No flags | Advanced |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
design-system-enforcer
itaisinai
Use before implementing new UI, copying/pasting component code, or styling components. Checks for duplicate patterns, hardcoded values, business logic in UI, and design system extraction opportunities.
add-feature
adamint
Adds new features to the application including API endpoints, React pages, components, and curriculum content. USE FOR: implementing new features, adding API endpoints, creating React components, adding curriculum content, writing unit/API tests. DO NOT USE FOR: E2E browser tests (use add-e2e-tests
react-modernization
wshobson
Upgrade React applications to latest versions, migrate from class components to hooks, and adopt concurrent features. Use when modernizing React codebases, migrating to React Hooks, or upgrading to latest React versions.
state-management
redpanda-data
Manage client and server state with Zustand stores and React Query patterns.
writing-react-effects
dust-tt
Writes React components without unnecessary useEffect. Use when creating/reviewing React components, refactoring effects, or when code uses useEffect to transform data or handle events.
form-refactorer
redpanda-data
Refactor legacy forms to use modern Redpanda UI Registry Field components with react-hook-form and Zod validation. Use when user requests: (1) Form refactoring or modernization, (2) Converting Chakra UI or @redpanda-data/ui forms, (3) Updating forms to use Field components, (4) Migrating from legacy form patterns, (5) Implementing forms with react-hook-form and Zod validation.