Skip to main content

Polymorphism

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

View as Markdown

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:

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:

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:

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.

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 for details.