PO

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.zip

Installs 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.
674 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

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

You give it
Base UI component name
You get back
Blazor headless component implementation with bUnit tests

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:

  1. Check the *DataAttributes.ts file if it exists — it may define additional custom-mapped attributes
  2. Also check the state shape (*State interface) — every truthy state field produces a data-* attribute via the default mapping
  3. Check for stateAttributesMapping overrides in the component's useRenderElement call — custom mappings (like fieldValidityMapping, popupStateMapping, transitionStatusMapping) replace the default for specific fields
  4. The absence of a *DataAttributes.ts file 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 useRenderElement renders)
  • ARIA attributes (roles, aria-expanded, aria-controls, aria-labelledby, etc.)
  • Data attributes (from *DataAttributes.ts and 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.forwardRef behavior)

Step 3: Read existing BlazeUI patterns

Before writing code, read the codebase to understand established patterns:

  1. Read CLAUDE.md and SESSION.md for architectural context and known issues
  2. Read an analogous existing component — pick one that matches the new component's category:
    • Overlay with popup: read Dialog or Popover components
    • Inline interactive: read Toggle or Checkbox components
    • Container with items: read Accordion or Tabs components
    • Form control: read NumberField or Select components
  3. Read the base classes you'll extend:
    • src/BlazeUI.Headless/Core/BlazeElement.cs for DOM-rendering components
    • src/BlazeUI.Headless/Core/OverlayRoot.cs for overlay root components
    • src/BlazeUI.Headless/Core/ComponentState.cs for controlled/uncontrolled state
  4. 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 typeBase classExamples
Overlay with popupOverlayRootDialog, Menu, Popover, Select, Tooltip
Inline interactiveBlazeElementToggle, Switch, Checkbox, NumberField
State-only containerComponentBaseAccordion, Tabs, NavigationMenu

For OverlayRoot subclasses, implement the abstract contract:

  • ModulePath — path to JS module
  • JsInstanceKey — unique instance key for JS
  • SyncContextState() — push open state onto context
  • OnJsInitializedAsync() — create DotNetRef, push JS refs onto context
  • OnDispose() — dispose DotNetRef
  • OnBeforeCloseAsync() — 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:

  1. Receives context via [CascadingParameter] internal {Component}Context Context { get; set; } = default!;
  2. Defines DefaultTag matching the Base UI default element
  3. Returns a state record from GetCurrentState()
  4. Yields data attributes from GetDataAttributes() — match Base UI's default state mapping and any *DataAttributes.ts overrides (see "Understanding Base UI's default state-to-data-attribute mapping" above)
  5. 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 Map keyed by instance ID for per-instance state
  • Export init(), action functions, and dispose(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 LibrarybUnit 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.

SkillInstallsUpdatedSafetyDifficulty
port-base-ui (this skill)04moReviewAdvanced
design-system-enforcer01moNo flagsIntermediate
add-feature03moReviewAdvanced
react-modernization212moNo flagsAdvanced

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.

00

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

00

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.

21134

state-management

redpanda-data

Manage client and server state with Zustand stores and React Query patterns.

16

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.

15

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.

13

Search skills

Search the agent skills registry