Components

SVG

Inline SVG renders the same DOM, in SVG's namespace, on every target, with SVG's case-exact element and attribute names.

An <svg> element starts SVG, and everything inside it is SVG. Write it as you would in HTML, with SVG's own names: every target renders the same elements, in SVG's namespace, from the server and in the browser.

StatusIcon.uf.tsx
export interface StatusIconProps {
  label: string;
  size?: number;
  colour?: string;
}

export default function StatusIcon({ label, size = 24, colour = "#1f6feb" }: StatusIconProps) {
  return (
    <svg class="status-icon" role="img" viewBox="0 0 24 24" width={size} height={size} fill="none">
      <title>{label}</title>
      <defs>
        <linearGradient id="status-icon-fill" x1="0" y1="0" x2="1" y2="1">
          <stop offset="0" stop-color={colour} />
          <stop offset="1" stop-color="#0b3d91" />
        </linearGradient>
      </defs>
      <circle cx="12" cy="12" r="10" fill="url(#status-icon-fill)" />
      <path
        d="M7 12.5l3 3 7-7"
        stroke="#ffffff"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      />
    </svg>
  );
}
StatusIcon.tsx
export interface StatusIconProps {
  label: string;
  size?: number;
  colour?: string;
}

export default function StatusIcon({ label, size = 24, colour = "#1f6feb" }: StatusIconProps) {
  return (
    <svg
      className="status-icon"
      role="img"
      viewBox="0 0 24 24"
      width={size}
      height={size}
      fill="none"
    >
      <title>{label}</title>
      <defs>
        <linearGradient id="status-icon-fill" x1="0" y1="0" x2="1" y2="1">
          <stop offset="0" stopColor={colour} />
          <stop offset="1" stopColor="#0b3d91" />
        </linearGradient>
      </defs>
      <circle cx="12" cy="12" r="10" fill="url(#status-icon-fill)" />
      <path
        d="M7 12.5l3 3 7-7"
        stroke="#ffffff"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

A childless SVG element closes itself in every output, and bound attributes follow the rules of Attributes: width={size} takes a number, and stop-color={colour} a string. React writes its own camelCase names for SVG's attributes, such as strokeWidth and stopColor. Angular writes SVG's <title> as <svg:title>: it reads an unprefixed title as the HTML element, whose content is text it does not parse.

Elements

Inside an <svg>, a tag is one of SVG's elements, in SVG's case: circle, clipPath, defs, desc, ellipse, the filter primitives (feBlend to feTurbulence), filter, g, image, line, linearGradient, marker, mask, metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect, stop, svg, switch, symbol, text, textPath, title, tspan, use and view. A name in the wrong case has a fix.

  • An HTML element inside an <svg>, or an SVG element outside one, is UF3001: the HTML parser leaves SVG at an HTML tag such as <div> or <p>.
  • <script>, <style> and SMIL animation (<animate>, <animateMotion>, <animateTransform>, <set>, <discard>) are UF3002: animation makes the rendering depend on time, and can set any attribute.
  • SVG's <a> and <foreignObject> are UF1002: the targets do not render them alike yet. So is MathML's <math>.
  • A component whose root is an SVG child, such as <g>, lands with composition in M3: it needs its parent's namespace.

Attributes

SVG's attribute names are case-exact, as SVG spells them: viewBox, preserveAspectRatio, and the presentation attributes in kebab case, such as stroke-width and stop-color. class, id, style, tabindex, lang, role, ARIA and data-* work as in HTML. <title> and <desc> take only class, id, lang, style, ARIA and data-*.

  • SVG has no boolean attributes, so a bare SVG attribute is UF3004.
  • href replaces SVG's old xlink:href, which is UF3004 with a fix. Other xlink: and xml: names, and xmlns, are UF3005: the targets render their namespaces differently.
  • A number in a style needs no unit only where CSS needs none. On SVG's stroke and opacity properties, Qwik adds px to a number and the other targets leave it bare, so strokeWidth: 2 is UF3018, with a fix that writes "2". The attribute, stroke-width="2", needs neither.

Text

SVG renders text only in <text>, <tspan>, <textPath>, <title> and <desc>, so an expression child can sit only there. Outside a <text>, text that is only whitespace is UF3003: Svelte drops it and the other targets keep it. Elsewhere the fix removes it, and a formatted source has none. In a <title> or a <desc>, a space between two values, as in <title>{name} {status}</title>, is part of the accessible name, so there is no fix: write the parts as one template literal, <title>{`${name} ${status}`}</title>.

A <title> labels its <svg> for assistive technology, as in the example, which also gives the <svg> role="img". <title> and <desc> hold text only: the HTML parser and Vue read an element inside them as HTML, and React as SVG (UF3003). Their text can still be built from several parts:

FileTransfer.uf.tsx
<title>
  {fileName}: {percent}% sent. {eta}
</title>
FileTransfer.tsx
<title>{`${fileName}: ${percent}% sent. ${eta ?? ""}`}</title>

React's server renderer writes a title's text only when its children are one value, and Qwik types a title's child as a string, so both join the parts into one template literal, with ?? "" for a part that may be absent. The other targets write the parts as they are.

The decision behind this page is ADR-0040.

Copyright © 2026