<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: angeloscle</title>
    <description>The latest articles on Forem by angeloscle (@angeloscle).</description>
    <link>https://forem.com/angeloscle</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1058978%2Fa7fe7209-141f-4d11-b56f-e7a8340ff31c.jpeg</url>
      <title>Forem: angeloscle</title>
      <link>https://forem.com/angeloscle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/angeloscle"/>
    <language>en</language>
    <item>
      <title>SMIL Animations in SVG: A Step-by-Step Guide Using a Real Wordmark</title>
      <dc:creator>angeloscle</dc:creator>
      <pubDate>Wed, 15 Apr 2026 11:17:10 +0000</pubDate>
      <link>https://forem.com/angeloscle/smil-animations-in-svg-a-step-by-step-guide-using-a-real-wordmark-28dd</link>
      <guid>https://forem.com/angeloscle/smil-animations-in-svg-a-step-by-step-guide-using-a-real-wordmark-28dd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is SMIL?&lt;/strong&gt; SMIL stands for &lt;strong&gt;Synchronized Multimedia Integration Language&lt;/strong&gt;. It is the W3C standard baked directly into SVG that lets you animate shapes, paths, transforms, colours, and more — &lt;strong&gt;zero JavaScript, zero CSS, zero external libraries required.&lt;/strong&gt; The browser does all the work natively inside the SVG element.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this guide we will build every animation you see in the &lt;strong&gt;roomie&lt;/strong&gt; wordmark below step by step, explaining &lt;em&gt;why&lt;/em&gt; each attribute exists and how to tweak it for your own projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────┐
│  r  [💬💬]  m  i  e                     │
│             ─────────                   │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 &lt;a href="https://roomieverse.net/img/roomie-blue.svg" rel="noopener noreferrer"&gt;View the live animated SVG&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two chat bubbles slide in from below and fade up into position when the SVG loads. The entire effect is ~30 lines of markup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The SVG canvas and viewport&lt;/li&gt;
&lt;li&gt;Defining reusable assets with &lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Painting with gradients&lt;/li&gt;
&lt;li&gt;Drawing the chat bubble paths&lt;/li&gt;
&lt;li&gt;Your first SMIL animation — &lt;code&gt;&amp;lt;animate&amp;gt;&lt;/code&gt; for opacity&lt;/li&gt;
&lt;li&gt;Moving things — &lt;code&gt;&amp;lt;animateTransform&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Controlling timing — &lt;code&gt;keyTimes&lt;/code&gt; and &lt;code&gt;begin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Natural easing — &lt;code&gt;calcMode&lt;/code&gt; and &lt;code&gt;keySplines&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Locking the final state — &lt;code&gt;fill="freeze"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Adding atmosphere — glow ellipse and accent line&lt;/li&gt;
&lt;li&gt;The finished file&lt;/li&gt;
&lt;li&gt;Common tasks and how to tackle them&lt;/li&gt;
&lt;li&gt;Browser support and gotchas&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. The SVG canvas and viewport
&lt;/h2&gt;

&lt;p&gt;Every SVG starts with its coordinate system. The &lt;code&gt;viewBox&lt;/code&gt; attribute maps internal coordinates to the rendered size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt;
  &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt;
  &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 420 160"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"420"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"160"&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"img"&lt;/span&gt;
  &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"roomie"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;roomie&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;viewBox="0 0 420 160"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Internal grid: 420 units wide, 160 units tall. All coordinates below live in this space.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;width&lt;/code&gt; / &lt;code&gt;height&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Rendered pixel size. Change these to scale the whole logo without touching a single coordinate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;role="img"&lt;/code&gt; + &lt;code&gt;aria-label&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Screen-reader accessibility. Always include these on decorative/logo SVGs.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Task tip:&lt;/strong&gt; Always design at a comfortable internal resolution (here 420 × 160) and rely on &lt;code&gt;viewBox&lt;/code&gt; scaling rather than hard-coding pixel sizes everywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Defining reusable assets with &lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt; is a container for things that are &lt;em&gt;defined&lt;/em&gt; but not &lt;em&gt;rendered&lt;/em&gt; until referenced via &lt;code&gt;url(#id)&lt;/code&gt;. Think of it as your SVG stylesheet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- gradients go here --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing inside &lt;code&gt;&amp;lt;defs&amp;gt;&lt;/code&gt; paints pixels on its own. It only activates when another element references it.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Painting with gradients
&lt;/h2&gt;

&lt;p&gt;The wordmark uses three gradient types. Here is each one explained.&lt;/p&gt;

&lt;h3&gt;
  
  
  3a. Radial gradient — the ambient glow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;radialGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"glow-b"&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"0.5"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"0.5"&lt;/span&gt; &lt;span class="na"&gt;r=&lt;/span&gt;&lt;span class="s"&gt;"0.55"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#3b82f6"&lt;/span&gt; &lt;span class="na"&gt;stop-opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.25"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#3b82f6"&lt;/span&gt; &lt;span class="na"&gt;stop-opacity=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/radialGradient&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cx&lt;/code&gt;, &lt;code&gt;cy&lt;/code&gt;, &lt;code&gt;r&lt;/code&gt; use the default &lt;code&gt;gradientUnits="objectBoundingBox"&lt;/code&gt;, so &lt;code&gt;0.5&lt;/code&gt; means &lt;em&gt;centre of the element that references this gradient&lt;/em&gt;. The gradient fades &lt;strong&gt;from 25% opacity blue in the centre to fully transparent at the edge&lt;/strong&gt; — a classic "light bloom" trick.&lt;/p&gt;

&lt;h3&gt;
  
  
  3b. Linear gradient — left bubble (indigo → slate)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"bubbleGradA-b"&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"0.9"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"0.9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#4f46e5"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0.5"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#3730a3"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#1e1b4b"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/linearGradient&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;x1/y1 → x2/y2&lt;/code&gt; defines the gradient direction as a diagonal line (top-left to bottom-right). Three stops give depth: a lighter highlight at the top-left, mid-tone in the centre, and a near-black shadow at the bottom-right.&lt;/p&gt;

&lt;h3&gt;
  
  
  3c. Linear gradient — right bubble (cyan → blue)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"bubbleGradB-b"&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"0.9"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"0.9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;    &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#06b6d4"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0.55"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#2563eb"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;    &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#4f46e5"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/linearGradient&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The direction is &lt;strong&gt;mirrored&lt;/strong&gt; (&lt;code&gt;x1="0.9"&lt;/code&gt; starts at the right): cyan hits the top-right edge, transitioning into the same indigo as the left bubble — they visually blend where they overlap.&lt;/p&gt;

&lt;h3&gt;
  
  
  3d. Linear gradient — text
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"textGrad-b"&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#e0e7ff"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0.4"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#ffffff"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0.7"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#a5f3fc"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#c7d2fe"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/linearGradient&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;x1="0" x2="1"&lt;/code&gt; with identical &lt;code&gt;y&lt;/code&gt; values = pure horizontal sweep. The stops produce a subtle shimmer: near-white in the middle with cool lavender on the left and cyan on the right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apply a gradient&lt;/strong&gt; by setting &lt;code&gt;fill="url(#gradient-id)"&lt;/code&gt; on any shape or text element.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Drawing the chat bubble paths
&lt;/h2&gt;

&lt;p&gt;Both bubbles use the SVG &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; element with Cubic Bézier curves (&lt;code&gt;C&lt;/code&gt; command).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt;
  &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M62 14
     C39 14 21 30 21 51
     C21 60 24.8 68 31 74
     C30.4 81.5 27.5 87.5 22 92.5
     C31 91.8 38.5 89.2 45 85
     C50 86.6 55.8 87.5 62 87.5
     C85 87.5 103 71.5 103 51
     C103 30 85 14 62 14Z"&lt;/span&gt;
  &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#bubbleGradA-b)"&lt;/span&gt;
  &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"rgba(129,140,248,0.20)"&lt;/span&gt;
  &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"1.2"&lt;/span&gt;
  &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Path anatomy:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;M62 14&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move to the starting point (top-centre of bubble)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;C x1 y1, x2 y2, x y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cubic Bézier curve: two control points then the end point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close the path back to the starting point&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "tail" at the bottom-left (points around &lt;code&gt;22 92.5&lt;/code&gt;) is drawn as a separate Bézier arc, giving the classic chat-bubble tail shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subtle inner shine:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ellipse&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"55"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"38"&lt;/span&gt; &lt;span class="na"&gt;rx=&lt;/span&gt;&lt;span class="s"&gt;"22"&lt;/span&gt; &lt;span class="na"&gt;ry=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"white"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.06"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A white ellipse at 6% opacity in the top-left of the bubble mimics light catching a glossy surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Your first SMIL animation — &lt;code&gt;&amp;lt;animate&amp;gt;&lt;/code&gt; for opacity
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;animate&amp;gt;&lt;/code&gt; element changes a &lt;em&gt;single numeric attribute&lt;/em&gt; of its parent over time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt;
  &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"opacity"&lt;/span&gt;
  &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"0; 1"&lt;/span&gt;
  &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 1"&lt;/span&gt;
  &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.35s"&lt;/span&gt;
  &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s"&lt;/span&gt;
  &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"freeze"&lt;/span&gt;
  &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place it &lt;strong&gt;as a direct child&lt;/strong&gt; of the element you want to animate — the &lt;code&gt;&amp;lt;g&amp;gt;&lt;/code&gt; that wraps the bubble path.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attributeName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opacity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which SVG attribute to animate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;values&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0; 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start at invisible, end at fully opaque&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keyTimes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0; 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Map each value to a position in the timeline (0 = start, 1 = end)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dur&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.35s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Total duration of this animation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;begin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fire immediately when the SVG is parsed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;freeze&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep the final value after the animation ends (explained in §9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;repeatCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Play once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Three-stop keyframe example:&lt;/strong&gt; if you wanted a &lt;em&gt;pulse&lt;/em&gt; then fade you'd write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt;
  &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"opacity"&lt;/span&gt;
  &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"0; 1; 0.5; 1"&lt;/span&gt;
  &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 0.3; 0.7; 1"&lt;/span&gt;
  &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"1s"&lt;/span&gt;
  &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"freeze"&lt;/span&gt;
  &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. Moving things — &lt;code&gt;&amp;lt;animateTransform&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;While &lt;code&gt;&amp;lt;animate&amp;gt;&lt;/code&gt; targets scalar attributes, &lt;code&gt;&amp;lt;animateTransform&amp;gt;&lt;/code&gt; handles CSS-transform-style operations: &lt;code&gt;translate&lt;/code&gt;, &lt;code&gt;rotate&lt;/code&gt;, &lt;code&gt;scale&lt;/code&gt;, &lt;code&gt;skewX&lt;/code&gt;, &lt;code&gt;skewY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The left bubble slides up from below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt;
  &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"transform"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"translate"&lt;/span&gt;
  &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"0 18; 0 -5; 0 0"&lt;/span&gt;
  &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 0.65; 1"&lt;/span&gt;
  &lt;span class="na"&gt;calcMode=&lt;/span&gt;&lt;span class="s"&gt;"spline"&lt;/span&gt;
  &lt;span class="na"&gt;keySplines=&lt;/span&gt;&lt;span class="s"&gt;"0.22 1 0.36 1; 0.5 0 0.5 1"&lt;/span&gt;
  &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.7s"&lt;/span&gt;
  &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s"&lt;/span&gt;
  &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"freeze"&lt;/span&gt;
  &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Breaking down &lt;code&gt;values&lt;/code&gt; for translate
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;values&lt;/code&gt; for &lt;code&gt;type="translate"&lt;/code&gt; uses &lt;code&gt;"x y"&lt;/code&gt; pairs separated by semicolons:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keyframe&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 18&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start 18 units &lt;em&gt;below&lt;/em&gt; final position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 -5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Overshoot 5 units &lt;em&gt;above&lt;/em&gt; final position (bounce)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Settle at the natural resting position&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three-point keyframe with an overshoot is the foundation of &lt;strong&gt;spring animation&lt;/strong&gt;. The bubble shoots past its target and gently falls back, feeling physical rather than mechanical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right bubble — slightly different timing:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;values="-8 21; -8 -3; -8 3"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;x&lt;/code&gt; component stays at &lt;code&gt;-8&lt;/code&gt; throughout (its group already has &lt;code&gt;transform="translate(-8, 3)"&lt;/code&gt;). The &lt;code&gt;y&lt;/code&gt; goes from &lt;code&gt;21&lt;/code&gt; → &lt;code&gt;-3&lt;/code&gt; → &lt;code&gt;3&lt;/code&gt;, a slightly shorter overshoot so the two bubbles settle with a staggered rhythm.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Controlling timing — &lt;code&gt;keyTimes&lt;/code&gt; and &lt;code&gt;begin&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;keyTimes="0; 0.65; 1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;keyTimes&lt;/code&gt; is a semicolon-separated list that maps each entry in &lt;code&gt;values&lt;/code&gt; to a point in the animation timeline, expressed as a fraction from &lt;code&gt;0&lt;/code&gt; (start) to &lt;code&gt;1&lt;/code&gt; (end).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Timeline:  |──────────────────────────────────|
           0                                  1
keyTimes:  0          0.65                   1
values:  "0 18"       "0 -5"               "0 0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;65% of the time&lt;/strong&gt; is spent on the main upward slide&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;35% of the time&lt;/strong&gt; is spent on the overshoot correction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Staggering animations with &lt;code&gt;begin&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- First element fires at 0s --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.7s"&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- A second element fires 150ms later --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.7s"&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0.15s"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also synchronise to another element's lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Fires when "bubble-a" ends --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"bubble-a.end"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. Natural easing — &lt;code&gt;calcMode&lt;/code&gt; and &lt;code&gt;keySplines&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;By default SMIL interpolates linearly (&lt;code&gt;calcMode="linear"&lt;/code&gt;). For organic motion, switch to &lt;code&gt;calcMode="spline"&lt;/code&gt; and provide cubic Bézier curves via &lt;code&gt;keySplines&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;calcMode="spline"
keySplines="0.22 1 0.36 1; 0.5 0 0.5 1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;keySplines&lt;/code&gt; entry corresponds to a &lt;em&gt;segment&lt;/em&gt; between consecutive keyframes. For three keyframes you have &lt;strong&gt;two segments&lt;/strong&gt;, hence two spline definitions separated by &lt;code&gt;;&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading a cubic Bézier: &lt;code&gt;x1 y1 x2 y2&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"0.22 1 0.36 1"   →   ease-out (slow finish)
"0.5 0 0.5 1"     →   ease-in-out (slow start AND slow finish)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can visualise these at &lt;a href="https://cubic-bezier.com" rel="noopener noreferrer"&gt;cubic-bezier.com&lt;/a&gt;. The equivalent CSS equivalents are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SMIL keySpline&lt;/th&gt;
&lt;th&gt;CSS equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 0 1 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;linear&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0.25 0.1 0.25 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ease&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0 0 0.2 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ease-out&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0.4 0 1 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ease-in&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0.22 1 0.36 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;custom spring-like ease-out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Quick cheat sheet for spring animation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Segment 1 (travel to overshoot): &lt;code&gt;0.22 1 0.36 1&lt;/code&gt; — fast exit, decelerates hard&lt;/li&gt;
&lt;li&gt;Segment 2 (correction): &lt;code&gt;0.5 0 0.5 1&lt;/code&gt; — symmetrical ease so the settle feels balanced&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  9. Locking the final state — &lt;code&gt;fill="freeze"&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;After an animation ends, the default behaviour (&lt;code&gt;fill="remove"&lt;/code&gt;) snaps the element back to its pre-animation value. That means your bubble would teleport back to &lt;code&gt;y=18&lt;/code&gt; and disappear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;fill="freeze"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fill="freeze"&lt;/code&gt; tells the animation engine to &lt;em&gt;hold the final keyframe value&lt;/em&gt; indefinitely. Always use this for entrance animations that you want to stay visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Adding atmosphere — glow ellipse and accent line
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ambient glow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ellipse&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"130"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"78"&lt;/span&gt; &lt;span class="na"&gt;rx=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;ry=&lt;/span&gt;&lt;span class="s"&gt;"55"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#glow-b)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This large translucent ellipse uses the radial gradient from §3a. Because the radial gradient fades to &lt;code&gt;stop-opacity="0"&lt;/code&gt; at its edge, the ellipse blends seamlessly into the background — no hard border.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accent line
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;line&lt;/span&gt;
  &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"14"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"134"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"130"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"134"&lt;/span&gt;
  &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"url(#bubbleGradB-b)"&lt;/span&gt;
  &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;
  &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;
  &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.35"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stroke-linecap="round"&lt;/code&gt; caps both ends of the line with half-circles, eliminating the harsh square nub the default produces. The line reuses &lt;code&gt;bubbleGradB-b&lt;/code&gt; so the cyan-to-indigo sweep echoes the right bubble above it.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. The finished file
&lt;/h2&gt;

&lt;p&gt;Here is the complete &lt;code&gt;roomie-blue.svg&lt;/code&gt; assembled from all the steps above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt;
     &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 420 160"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"420"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"160"&lt;/span&gt;
     &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"img"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"roomie"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;roomie&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;desc&amp;gt;&lt;/span&gt;Roomie wordmark with chat bubbles.&lt;span class="nt"&gt;&amp;lt;/desc&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Ambient glow --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;radialGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"glow-b"&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"0.5"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"0.5"&lt;/span&gt; &lt;span class="na"&gt;r=&lt;/span&gt;&lt;span class="s"&gt;"0.55"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#3b82f6"&lt;/span&gt; &lt;span class="na"&gt;stop-opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.25"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#3b82f6"&lt;/span&gt; &lt;span class="na"&gt;stop-opacity=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/radialGradient&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Left bubble: deep indigo → slate blue --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"bubbleGradA-b"&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"0.9"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"0.9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#4f46e5"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0.5"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#3730a3"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#1e1b4b"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/linearGradient&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Right bubble: electric blue → cyan --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"bubbleGradB-b"&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"0.9"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"0.9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;    &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#06b6d4"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0.55"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#2563eb"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;    &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#4f46e5"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/linearGradient&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Text shimmer --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"textGrad-b"&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#e0e7ff"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0.4"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#ffffff"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"0.7"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#a5f3fc"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;   &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#c7d2fe"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/linearGradient&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Ambient glow bloom --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ellipse&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"130"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"78"&lt;/span&gt; &lt;span class="na"&gt;rx=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;ry=&lt;/span&gt;&lt;span class="s"&gt;"55"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#glow-b)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- "r" wordmark --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"8"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"120"&lt;/span&gt;
        &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#textGrad-b)"&lt;/span&gt;
        &lt;span class="na"&gt;font-family=&lt;/span&gt;&lt;span class="s"&gt;"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"&lt;/span&gt;
        &lt;span class="na"&gt;font-weight=&lt;/span&gt;&lt;span class="s"&gt;"800"&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"118"&lt;/span&gt; &lt;span class="na"&gt;letter-spacing=&lt;/span&gt;&lt;span class="s"&gt;"-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;r&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Chat bubbles group --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(28, 26)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Left bubble --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;g&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt; &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"transform"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"translate"&lt;/span&gt;
        &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"0 18; 0 -5; 0 0"&lt;/span&gt; &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 0.65; 1"&lt;/span&gt;
        &lt;span class="na"&gt;calcMode=&lt;/span&gt;&lt;span class="s"&gt;"spline"&lt;/span&gt; &lt;span class="na"&gt;keySplines=&lt;/span&gt;&lt;span class="s"&gt;"0.22 1 0.36 1; 0.5 0 0.5 1"&lt;/span&gt;
        &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.7s"&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"freeze"&lt;/span&gt; &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt; &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"opacity"&lt;/span&gt; &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"0; 1"&lt;/span&gt;
        &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 1"&lt;/span&gt; &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.35s"&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"freeze"&lt;/span&gt; &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt;
        &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M62 14 C39 14 21 30 21 51 C21 60 24.8 68 31 74
           C30.4 81.5 27.5 87.5 22 92.5 C31 91.8 38.5 89.2 45 85
           C50 86.6 55.8 87.5 62 87.5 C85 87.5 103 71.5 103 51
           C103 30 85 14 62 14Z"&lt;/span&gt;
        &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#bubbleGradA-b)"&lt;/span&gt;
        &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"rgba(129,140,248,0.20)"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"1.2"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ellipse&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"55"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"38"&lt;/span&gt; &lt;span class="na"&gt;rx=&lt;/span&gt;&lt;span class="s"&gt;"22"&lt;/span&gt; &lt;span class="na"&gt;ry=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"white"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.06"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/g&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Right bubble --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;transform=&lt;/span&gt;&lt;span class="s"&gt;"translate(-8, 3)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt; &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"transform"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"translate"&lt;/span&gt;
        &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"-8 21; -8 -3; -8 3"&lt;/span&gt; &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 0.7; 1"&lt;/span&gt;
        &lt;span class="na"&gt;calcMode=&lt;/span&gt;&lt;span class="s"&gt;"spline"&lt;/span&gt; &lt;span class="na"&gt;keySplines=&lt;/span&gt;&lt;span class="s"&gt;"0.22 1 0.36 1; 0.5 0 0.5 1"&lt;/span&gt;
        &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.85s"&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"freeze"&lt;/span&gt; &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt; &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"opacity"&lt;/span&gt; &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"0; 1"&lt;/span&gt;
        &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 1"&lt;/span&gt; &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.35s"&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"freeze"&lt;/span&gt; &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt;
        &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M132 17 C112 17 96 31.5 96 50 C96 58 99 65 104.5 70.5
           C104 77.5 101.5 83 97 87.5 C104.5 87 111.5 84.8 117.5 81
           C121.5 82.3 126 83 131 83 C151 83 167 68.5 167 50
           C167 31.5 151 17 132 17Z"&lt;/span&gt;
        &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#bubbleGradB-b)"&lt;/span&gt;
        &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"rgba(34,211,238,0.15)"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"1.2"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ellipse&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"138"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"37"&lt;/span&gt; &lt;span class="na"&gt;rx=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; &lt;span class="na"&gt;ry=&lt;/span&gt;&lt;span class="s"&gt;"8"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"white"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.07"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/g&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Connection spark --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;circle&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"96"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt; &lt;span class="na"&gt;r=&lt;/span&gt;&lt;span class="s"&gt;"2.5"&lt;/span&gt;  &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#67e8f9"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.5"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;circle&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"96"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt; &lt;span class="na"&gt;r=&lt;/span&gt;&lt;span class="s"&gt;"5"&lt;/span&gt;    &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#67e8f9"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.12"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/g&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- "mie" wordmark --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"178"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"120"&lt;/span&gt;
        &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#textGrad-b)"&lt;/span&gt;
        &lt;span class="na"&gt;font-family=&lt;/span&gt;&lt;span class="s"&gt;"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"&lt;/span&gt;
        &lt;span class="na"&gt;font-weight=&lt;/span&gt;&lt;span class="s"&gt;"800"&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"118"&lt;/span&gt; &lt;span class="na"&gt;letter-spacing=&lt;/span&gt;&lt;span class="s"&gt;"-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;mie&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Accent underline --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"14"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"134"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"130"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"134"&lt;/span&gt;
        &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"url(#bubbleGradB-b)"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;
        &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;opacity=&lt;/span&gt;&lt;span class="s"&gt;"0.35"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  12. Common tasks and how to tackle them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Task: Make the animation loop forever
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Change repeatCount="1" → "indefinite" on both animations --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"indefinite"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt;          &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"indefinite"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a looping entrance you'll also want &lt;code&gt;fill="remove"&lt;/code&gt; (the default) so each iteration resets to the start value.&lt;/p&gt;




&lt;h3&gt;
  
  
  Task: Trigger the animation on hover (CSS + SMIL hybrid)
&lt;/h3&gt;

&lt;p&gt;SMIL &lt;code&gt;begin&lt;/code&gt; accepts event-based triggers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt;
  &lt;span class="err"&gt;...&lt;/span&gt;
  &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"mouseover"&lt;/span&gt;
  &lt;span class="na"&gt;end=&lt;/span&gt;&lt;span class="s"&gt;"mouseout"&lt;/span&gt;
  &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"freeze"&lt;/span&gt;
  &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;&amp;lt;img src="logo.svg"&amp;gt;&lt;/code&gt; embeds this won't fire (the SVG is sandboxed). Use the SVG &lt;strong&gt;inline&lt;/strong&gt; in HTML or via &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; for event-based triggers.&lt;/p&gt;




&lt;h3&gt;
  
  
  Task: Add a colour-cycling animation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt;
  &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"stop-color"&lt;/span&gt;
  &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"#4f46e5; #06b6d4; #4f46e5"&lt;/span&gt;
  &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 0.5; 1"&lt;/span&gt;
  &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"4s"&lt;/span&gt;
  &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"indefinite"&lt;/span&gt;
  &lt;span class="na"&gt;calcMode=&lt;/span&gt;&lt;span class="s"&gt;"spline"&lt;/span&gt;
  &lt;span class="na"&gt;keySplines=&lt;/span&gt;&lt;span class="s"&gt;"0.4 0 0.6 1; 0.4 0 0.6 1"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place this directly inside a &lt;code&gt;&amp;lt;stop&amp;gt;&lt;/code&gt; element inside a gradient. Note: &lt;code&gt;attributeName="stop-color"&lt;/code&gt; uses the hyphenated CSS property name, not a presentation attribute.&lt;/p&gt;




&lt;h3&gt;
  
  
  Task: Rotate an element around its own centre
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;animateTransform type="rotate"&amp;gt;&lt;/code&gt; values format: &lt;code&gt;"angle cx cy"&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Spin the connection spark 360° around its own centre --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt;
  &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"transform"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"rotate"&lt;/span&gt;
  &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"0 96 50; 360 96 50"&lt;/span&gt;
  &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"3s"&lt;/span&gt;
  &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"indefinite"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;96 50&lt;/code&gt; are the &lt;code&gt;cx&lt;/code&gt;/&lt;code&gt;cy&lt;/code&gt; of the circle — the pivot point.&lt;/p&gt;




&lt;h3&gt;
  
  
  Task: Scale an element for a "pulse" badge effect
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;animateTransform&lt;/span&gt;
  &lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;"transform"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"scale"&lt;/span&gt;
  &lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;"1; 1.15; 1"&lt;/span&gt;
  &lt;span class="na"&gt;keyTimes=&lt;/span&gt;&lt;span class="s"&gt;"0; 0.5; 1"&lt;/span&gt;
  &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"1.4s"&lt;/span&gt;
  &lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;"indefinite"&lt;/span&gt;
  &lt;span class="na"&gt;calcMode=&lt;/span&gt;&lt;span class="s"&gt;"spline"&lt;/span&gt;
  &lt;span class="na"&gt;keySplines=&lt;/span&gt;&lt;span class="s"&gt;"0.4 0 0.6 1; 0.4 0 0.6 1"&lt;/span&gt;
  &lt;span class="na"&gt;additive=&lt;/span&gt;&lt;span class="s"&gt;"sum"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;additive="sum"&lt;/code&gt; is critical when the element already has a &lt;code&gt;transform&lt;/code&gt; attribute — it &lt;strong&gt;adds&lt;/strong&gt; to the existing transform instead of replacing it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Task: Stagger multiple elements with &lt;code&gt;begin&lt;/code&gt; offsets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Element 1: fires at 0s --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"a1"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s"&lt;/span&gt;    &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.6s"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Element 2: fires after element 1 ends --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt;         &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"a1.end"&lt;/span&gt; &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.6s"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Element 3: fires 0.2s after element 1 ends --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt;         &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"a1.end+0.2s"&lt;/span&gt; &lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;"0.6s"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The roomie logo uses a simpler form — both bubbles start at &lt;code&gt;begin="0s"&lt;/code&gt; but have different &lt;code&gt;dur&lt;/code&gt; values (&lt;code&gt;0.7s&lt;/code&gt; vs &lt;code&gt;0.85s&lt;/code&gt;), creating a natural stagger through speed difference rather than delayed starts.&lt;/p&gt;




&lt;h3&gt;
  
  
  Task: Debug an animation that isn't playing
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check &lt;code&gt;repeatCount="1"&lt;/code&gt; + &lt;code&gt;fill="freeze"&lt;/code&gt;&lt;/strong&gt; — without &lt;code&gt;fill="freeze"&lt;/code&gt; the element returns to its original state and looks broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm the SVG is inline or in &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags block SMIL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect &lt;code&gt;keyTimes&lt;/code&gt; count&lt;/strong&gt; — must equal the count of semicolon-delimited values in &lt;code&gt;values&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect &lt;code&gt;keySplines&lt;/code&gt; count&lt;/strong&gt; — must equal &lt;code&gt;keyTimes count - 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox quirk&lt;/strong&gt; — &lt;code&gt;attributeName="transform"&lt;/code&gt; must be lowercase; Firefox is case-sensitive.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  13. Browser support and gotchas
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser&lt;/th&gt;
&lt;th&gt;SMIL support&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chrome / Edge&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;Chromium has had full SMIL support since 2013&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;Case-sensitive attribute names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safari&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;iOS Safari too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IE 11&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;IE never supported SMIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; embed&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;td&gt;Declarative animations play; event-based triggers (&lt;code&gt;mouseover&lt;/code&gt;) do not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;background-image&lt;/code&gt; CSS&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;SVG animations are blocked entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use SMIL when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want self-contained SVG files (logos, icons, loaders)&lt;/li&gt;
&lt;li&gt;You need animations in email clients that support SVG&lt;/li&gt;
&lt;li&gt;You want zero runtime dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use CSS animations instead when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need &lt;code&gt;prefers-reduced-motion&lt;/code&gt; media query support (SMIL ignores it by default — add JS to pause if needed)&lt;/li&gt;
&lt;li&gt;You want DevTools animation inspector support&lt;/li&gt;
&lt;li&gt;You're animating SVGs that are part of a larger component tree&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;SMIL gives you a remarkably complete animation toolkit inside a plain XML file. The roomie wordmark shows how much you can achieve in ~30 lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Gradient fills (radial + linear)&lt;/li&gt;
&lt;li&gt;✅ Smooth entrance animation with spring overshoot&lt;/li&gt;
&lt;li&gt;✅ Opacity fade-in&lt;/li&gt;
&lt;li&gt;✅ Natural cubic Bézier easing&lt;/li&gt;
&lt;li&gt;✅ Staggered multi-element timing&lt;/li&gt;
&lt;li&gt;✅ Atmospheric glow and subtle shines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mental model is simple: put your &lt;code&gt;&amp;lt;animate&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;animateTransform&amp;gt;&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; the element it should affect, describe &lt;em&gt;what&lt;/em&gt; changes in &lt;code&gt;values&lt;/code&gt;, &lt;em&gt;when&lt;/em&gt; in &lt;code&gt;keyTimes&lt;/code&gt;, and &lt;em&gt;how fast&lt;/em&gt; in &lt;code&gt;dur&lt;/code&gt;. The rest is taste.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions or want to share your own SMIL animation? Drop them in the comments below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>svg</category>
      <category>animation</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Run a node.js app with systemd</title>
      <dc:creator>angeloscle</dc:creator>
      <pubDate>Sun, 24 Nov 2024 19:04:15 +0000</pubDate>
      <link>https://forem.com/angeloscle/run-nodejs-app-with-systemd-3g6b</link>
      <guid>https://forem.com/angeloscle/run-nodejs-app-with-systemd-3g6b</guid>
      <description>&lt;p&gt;Using systemd to run your Node.js app simplifies and streamlines the process, eliminating the need for additional scripts to manage your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the node.js server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;; // listen on all ports
const port = 3311;

http.createServer((req, res) =&amp;gt; {

  res.writeHead(200, { &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; });
  res.end(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;Node&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;);

}).listen(port, hostname, () =&amp;gt; {

  console.log(`Server running at http://${hostname}:${port}/`);

});
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can save this file under the path /home/node/server.js and make sure you can it run within console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;nodejs /home/myserver/server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server running at http://127.0.0.1:3311/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the service file
&lt;/h2&gt;

&lt;p&gt;Create a service file on /etc/systemd/system/&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /etc/systemd/system/
&lt;span class="nv"&gt;$ &lt;/span&gt;nano node.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;File content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Start Node.js App
&lt;span class="nt"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;network.target

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/usr/bin/npm start
&lt;span class="nt"&gt;KillMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;control-group
&lt;span class="nt"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/home/myserver/
&lt;span class="nt"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;on-failure

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;A line-by-line explanation of the above file's content:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[Unit]&lt;/strong&gt;&lt;br&gt;
The &lt;strong&gt;[Unit]&lt;/strong&gt; section provides metadata and dependencies for the unit file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Description=Start Node.js App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A short description of the service. This will appear in systemctl commands like systemctl status node.service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After=network.target&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Specifies that this service should start only after the network.target is active. It ensures the network is up before your Node.js app starts, which is often necessary for apps relying on networking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[Service]&lt;/strong&gt;&lt;br&gt;
The &lt;strong&gt;[Service]&lt;/strong&gt; section defines how the service behaves when it starts, stops, or restarts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ExecStart=/usr/bin/npm start&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The command to execute when starting the service. In this case, it runs npm start from the Node.js application's working directory. This assumes your package.json file has a start script configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;KillMode=control-group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Controls how processes in the service's cgroup are terminated. control-group means that systemd will kill all processes in the control group (not just the main process) when stopping the service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WorkingDirectory=/home/myserver/&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The directory where the service runs. This is typically the root directory of your Node.js application, containing files like package.json.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Restart=on-failure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Configures the service to automatically restart if it exits with a non-zero exit code (indicating failure). This improves the reliability of the service by ensuring it stays running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[Install]&lt;/strong&gt;&lt;br&gt;
The &lt;strong&gt;[Install]&lt;/strong&gt; section specifies how and when the service should be started during the system boot process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WantedBy=multi-user.target&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Indicates that this service should be started in the multi-user.target environment, which is a common system state for non-graphical, multi-user systems. This ensures the service starts automatically when the system boots into this state.&lt;/p&gt;
&lt;h2&gt;
  
  
  Enable the service
&lt;/h2&gt;

&lt;p&gt;Enable the service to start on server boot&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;node.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start the service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl start node.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verify it is running
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl status node.service
&lt;span class="nv"&gt;$ &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; node.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  **Journalctl
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A tool which interacts with the systemd journal for querying logs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&amp;amp; a useful journalctl command&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-xfeu&lt;/span&gt; node.service &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"2024-11-13 00:00:00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;--since "2024-11-13 00:00:00"&lt;/strong&gt;&lt;br&gt;
With the above &lt;em&gt;option&lt;/em&gt; of the command you can limit the logs to entries that occurred after the specified date and time (2024-11-13 00:00:00). This ensures you only see logs from a specific time frame onward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-f:&lt;/strong&gt; Follows the log in real time. Similar to tail -f, this allows you to see new log entries as they are written&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-e&lt;/strong&gt;: Jumps to the end of the logs, showing the most recent entries. This is useful when combined with -f&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;-u node.service&lt;/strong&gt;: Filters logs for a specific systemd service (node.service in this case). Only log entries related to node.service will be shown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://roomieverse.net" rel="noopener noreferrer"&gt;https://roomieverse.net&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
