---
title: Polymorphism
description: Render components as alternative HTML elements or custom SolidJS
  components with full type safety via the as prop.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Polymorphism

> Render components as alternative HTML elements or custom SolidJS components with full type safety via the as prop.

Moraine components and parts that expose `as` can render a different element or Solid component while preserving their styles and typed props. State-only roots and native form controls such as `Input` do not expose `as`. When changing an element, ensure the result still provides an accessible name and appropriate semantics.

## Rendering as an HTML Element (`as="tag"`)

By default, interactive components render sensible semantic elements (such as `<button>` for `Button` or `<div>` for `Card`). Use `as="tag"` to change the underlying HTML element:

```tsx
import { Button } from 'moraine'

export function NavigationLink() {
  return (
    // Renders an <a> element with Button styles and anchor attributes:
    <Button as="a" href="/docs/getting-started" target="_blank">
      Documentation
    </Button>
  )
}
```

### TypeScript Attribute Narrowing

When you specify `as="tag"`, Moraine automatically narrows the allowed props, event types, and `ref` callback parameter to match that specific HTML tag:

```tsx
import { Button } from 'moraine'

// Valid: 'href' and 'target' exist on <a>
<Button as="a" href="/dashboard" target="_blank">Dashboard</Button>

// Type Error: Property 'href' does not exist on HTMLButtonElement
<Button as="button" href="/dashboard">Dashboard</Button>
```

## Rendering as a Solid Component (`as={Component}`)

You can pass another SolidJS component to `as`. Moraine preserves the target component's custom props, providing full IDE autocompletion, type validation, and direct props forwarding:

```tsx
import { Button, DropdownMenu } from 'moraine'

export function MenuExample() {
  return (
    <DropdownMenu>
      {/* Renders Button with its 'variant', 'size', and 'loadingAuto' props intact */}
      <DropdownMenu.Trigger as={Button} variant="ghost" size="icon-xs">
        Menu
      </DropdownMenu.Trigger>
      <DropdownMenu.Content items={[{ label: 'Profile' }, { label: 'Settings' }]} />
    </DropdownMenu>
  )
}
```

Custom components receive ordinary event props and can wrap, conditionally forward, or invoke them. Forward the root `ref` to let Moraine resolve native element semantics and manage focus. Ordinary event callbacks remain available even when the target does not forward its ref.

## Overlay Triggers and Composition

Overlay triggers (`Dialog.Trigger`, `Popover.Trigger`, `Sheet.Trigger`, `DropdownMenu.Trigger`, `ContextMenu.Trigger`) are designed to be composed with `as={Button}`:

1. **State Injection**: The trigger automatically injects `aria-haspopup`, `aria-expanded`, and required click or pointer handlers into the rendered element.
2. **Event Merging**: If you provide your own `onClick` handler on `<Dialog.Trigger as={Button} onClick={...}>`, Moraine calls your handler first. If you call `event.preventDefault()`, the disclosure action is canceled.
3. **Ref Forwarding**: Refs point directly to the underlying DOM button, allowing easy focus management.

Modal, Dialog, Sheet, Popover, Tooltip, and DropdownMenu triggers handle composed clicks after a custom root's own click handler. For these triggers, the custom target does not receive the composed `onClick` prop; its own DOM click handler can call `preventDefault()` to cancel activation. Other polymorphic roots, including BaseSelect and ContextMenu triggers, forward `onClick` normally. Keyboard handlers run through the target's forwarding path in both the main document and iframe documents.

```tsx
import { Button, Dialog } from 'moraine'

export function DeleteConfirmation() {
  let triggerRef: HTMLButtonElement | undefined

  return (
    <Dialog>
      <Dialog.Trigger
        as={Button}
        variant="destructive"
        ref={(el) => (triggerRef = el)}
        onClick={() => console.log('Opening delete modal')}
      >
        Delete Account
      </Dialog.Trigger>
      <Dialog.Content title="Are you sure?">
        <Dialog.Body>This action cannot be undone.</Dialog.Body>
      </Dialog.Content>
    </Dialog>
  )
}
```

## Global Type Configuration

If you want to restrict `as` autocompletion to standard HTML element tags (excluding SVG and MathML elements), enable `simpleHtmlTags` via module augmentation. See the [TypeScript Guide](https://moraine.subf.dev/docs/typescript.md#simple-html-tags) for details.

