Polymorphic as prop in Astro

chantastic - Aug 31 '23 - - Dev Community

Astro has dynamic tags — a way to take an element or component as a prop.

I had trouble finding the feature because the React community calls this pattern the "polymorphic as prop". Because we enjoy our pseudo-computer-science bullshit artisinally.

Dynamic tags are simple to implement in Astro.

No-fuss implementation

  1. Take a capitalized Element prop as a local variable.
  2. Render that prop as a template tag.
  3. Take and spread rest-props.
  4. Render children using Astro's un-named <slot />.
---
const { Element, ...props } = Astro.props;
---

<Element {...props}><slot /></Element>

Enter fullscreen mode Exit fullscreen mode

Destructure and rename the element prop (for convenience or convention)

Uppercase prop names can look out of place in templates. Destructure and rename the Element prop in one to provide a more ergonomic/conventional authoring experience.

---
const { as: Element, ...props } = Astro.props;
---

<Element {...props}><slot /></Element>

Enter fullscreen mode Exit fullscreen mode

Astro templates require that dynamic tags be capitalized. Renaming the element prop is a convent way to follow that requirement while providing a more conventional API to consumers.

Accept and de-duplicate classes with class:list

Astro has a class:list directive for orchestrating dynamic classes. Provide the class:list directive an array with both provided and component classes.

class:list is smart and automatically removes duplicate classes.

---
const {
  as: Element = "div",
  class: providedProps,
  ...props
} = Astro.props;
const componentClasses = "prose prose-slate dark:prose-invert";
---

<Element {...props} class:list={[componentClasses, providedProps]}>
  <slot />
</Element>

Enter fullscreen mode Exit fullscreen mode

Note: class needs to be renamed when destructured because values can not be assigned to reserved words.

Complete with TypeScript interface

This is my completed component, with the TypeScript interface.

Yours needs will likely vary.

---
interface Props {
  as?: "body" | "main" | "article";
  class?: "string";
}

const {
  as: Element = "div",
  class: providedProps,
  ...props
} = Astro.props;
---

<Element {...props} class:list={[componentClasses, providedProps]}>
  <slot />
</Element>

Enter fullscreen mode Exit fullscreen mode

BEWARE: dynamic tags don't honor hydration client directives

Astro provides client directives for hydrating client-side UI. Those don't work with dynamic tags.

If you're using dynamic tags for static layouts — like me — this isn't an issue.

Takeaways

Astro supports the "polymorphic as prop" pattern popular in React. And the additional standard tooling of TypeScript and class:list directive make it even easier to consistently implement.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .