Components

Dynamic components

<component is> renders one of a set of tags or components that the compiler knows, chosen by a condition or a prop. Each framework gets its own dynamic element, and Angular a switch over the candidates.

Sometimes the element itself depends on the data: a heading whose level follows its depth in an outline, a section title that drops a level when it is nested, a card that collapses into a chip. <component is> renders one of a set of tags, or one of a set of components, chosen by a condition or by a prop.

Choosing a tag

is takes a conditional whose branches are tag names. The attributes and children apply to whichever tag renders:

Section.uf.tsx
import { ref } from "unframework";

export default function Section({ title }: { title: string }) {
  const nested = ref(false);
  return (
    <article>
      <component is={nested.value ? "h3" : "h2"} class="title">
        {title}
      </component>
      <button type="button" onClick={() => (nested.value = !nested.value)}>
        {nested.value ? "Lift" : "Nest"}
      </button>
    </article>
  );
}
Section.tsx
import { type CSSProperties, useRef, useState } from "react";

export interface SectionAttributes {
  className?: string;
  style?: CSSProperties;
}

export default function Section({
  title,
  className,
  style,
}: { title: string } & SectionAttributes) {
  const [nested, setNested] = useState(false);
  const nestedRef = useRef(nested);
  const Tag = nested ? "h3" : "h2";

  return (
    <article className={className} style={style}>
      <Tag className="title">{title}</Tag>
      <button
        type="button"
        onClick={() => {
          nestedRef.current = !nestedRef.current;
          setNested(nestedRef.current);
        }}
      >
        {nested ? "Lift" : "Nest"}
      </button>
    </article>
  );
}
  • React, Qwik and Astro hold the chosen tag in a constant, const Tag = nested ? "h3" : "h2", and render <Tag>.
  • Vue writes <component :is>, Svelte <svelte:element this={…}>, and Solid <Dynamic component={…}>.
  • A strict Angular template can't choose an element by value, so Angular writes an @switch with one @case per tag, each with the attributes and children (dynamic-component, emulated).

Every attribute must be valid on every candidate, since any of them may render: an attribute one tag doesn't take is UF3006, for that tag.

A prop typed as a union of tag names can choose the tag too, as a heading's level:

Heading.uf.tsx
export default function Heading({ tag, text }: { tag: "h1" | "h2" | "h3"; text: string }) {
  return (
    <component is={tag} class="heading">
      {text}
    </component>
  );
}

Choosing a component

The candidates can be components instead, imported or of the same module. Each one must declare every prop, model and slot you pass:

Tag.uf.tsx
import { ref } from "unframework";

import Card from "./Card.uf.tsx";
import Chip from "./Chip.uf.tsx";

export default function Tag({ label }: { label: string }) {
  const compact = ref(true);
  return (
    <section aria-label="Tag">
      <component is={compact.value ? Chip : Card} label={label} />
      <button type="button" onClick={() => (compact.value = !compact.value)}>
        {compact.value ? "Expand" : "Compact"}
      </button>
    </section>
  );
}

React, Qwik and Astro hold the chosen component in a constant, Vue writes <component :is="compact ? Chip : Card">, Svelte a $derived component, Solid <Dynamic>, and Angular an @switch with a @case per component. An attribute or a fill that one of the candidates doesn't declare is UF3035 or UF3038, as on any component.

A known set

is chooses from a set the compiler knows when it compiles: a conditional or a literal whose leaves are all tags or all components, or a prop typed as a union of tag literals. Angular writes every candidate, and the typed targets check each one, so a value only the run time knows, such as a string from an API, is UF3044, and so is a set that mixes tags and components. Write the candidates out in a conditional, or type the prop as a union of tags.

The decisions behind this page are ADR-0054 and ADR-0055.

Copyright © 2026