<?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>DEV Community: defunkt-dev</title>
    <description>The latest articles on DEV Community by defunkt-dev (@defunktdev).</description>
    <link>https://dev.to/defunktdev</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023504%2Ffcb3eacf-8c3c-4fc1-9030-cdc50369c8ac.png</url>
      <title>DEV Community: defunkt-dev</title>
      <link>https://dev.to/defunktdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/defunktdev"/>
    <language>en</language>
    <item>
      <title>auto-animate now speaks marko</title>
      <dc:creator>defunkt-dev</dc:creator>
      <pubDate>Wed, 15 Jul 2026 08:50:08 +0000</pubDate>
      <link>https://dev.to/defunktdev/auto-animate-now-speaks-marko-5c7</link>
      <guid>https://dev.to/defunktdev/auto-animate-now-speaks-marko-5c7</guid>
      <description>&lt;p&gt;As of &lt;code&gt;@formkit/auto-animate@0.10.0&lt;/code&gt;, the library ships an official Marko adapter (&lt;a href="https://github.com/formkit/auto-animate/pull/239" rel="noopener noreferrer"&gt;PR #239&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;If you write Marko 6, you can now get animated list additions, removals, and reorders with one tag and no per-item work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @formkit/auto-animate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import AutoAnimate from "@formkit/auto-animate/marko"

&amp;lt;let/items = [
  { id: 1, text: "Mango" },
  { id: 2, text: "Papaya" },
]&amp;gt;

&amp;lt;ul/listRef&amp;gt;
  &amp;lt;for|item| of=items by="id"&amp;gt;
    &amp;lt;li&amp;gt;${item.text}&amp;lt;/li&amp;gt;
  &amp;lt;/for&amp;gt;
&amp;lt;/ul&amp;gt;
&amp;lt;AutoAnimate parent=listRef/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole integration. Mutate &lt;code&gt;items&lt;/code&gt; however you like (push, filter, reverse, shuffle) and the DOM changes animate. There's a live demo covering the full API, server-rendered with &lt;code&gt;@marko/run&lt;/code&gt;: &lt;strong&gt;&lt;a href="https://codesandbox.io/p/github/defunkt-dev/auto-animate-marko-demo/main" rel="noopener noreferrer"&gt;demo&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://github.com/defunkt-dev/auto-animate-marko-demo" rel="noopener noreferrer"&gt;source&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The rest of this post is the part that's actually interesting: what shape an auto-animate adapter should take in a framework that has no directives, no hooks, does not perform a hydration re-render during initialization/resume (it resumes the server-rendered DOM in place) and what we learned when the published package met the newest Vite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marko in sixty seconds
&lt;/h2&gt;

&lt;p&gt;If you haven't met &lt;a href="https://markojs.com/" rel="noopener noreferrer"&gt;Marko&lt;/a&gt;: it's an HTML-based language for building reactive UIs, born at eBay (it powers eBay.com) and now under the OpenJS Foundation. Nearly any valid HTML is valid Marko; you add reactivity with tags instead of a component API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;let/count=0&amp;gt;
&amp;lt;button onClick() { count++ }&amp;gt;
  Clicked ${count} times
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties matter for this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fine-grained by compilation.&lt;/strong&gt; The compiler analyzes which parts of a template are actually interactive and ships JavaScript only for those, at sub-template granularity. Static content ships zero JS. This goes further than islands: you don't declare server versus client components, the compiler works it out from the state tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resumable, not hydrated.&lt;/strong&gt; The server's HTML &lt;em&gt;is&lt;/em&gt; the app. The client picks it up where the server left off, with no re-render pass to rebuild a component tree. (This is the property the SSR section below hinges on.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiny output.&lt;/strong&gt; In an &lt;a href="https://www.lorenstew.art/blog/10-kanban-boards" rel="noopener noreferrer"&gt;independent comparison&lt;/a&gt; that built the same kanban app in ten frameworks, Marko shipped the smallest bundles of all: 6.8 kB compressed for the homepage and 28.8 kB for the full interactive board, versus 176.1 kB for Next.js. Our own numbers agree: every fully interactive route in the demo linked above ships about 10 kB of gzipped JavaScript in total, framework runtime &lt;em&gt;and&lt;/em&gt; animation library included. Marko's runtime is estimated to be 7KB Gzipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Marko also streams HTML (in-order and out-of-order) as it's ready and has built-in TypeScript support. &lt;code&gt;@marko/run&lt;/code&gt; is its file-based SSR meta-framework, which the demo uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What auto-animate is (and isn't)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/formkit/auto-animate/" rel="noopener noreferrer"&gt;auto-animate&lt;/a&gt; does exactly one thing: when the direct children of an element are added, removed, or moved, it FLIP-animates the change. No springs, no gestures, no orchestration, no timelines. Its about 14K stars are for the effort-to-payoff ratio, not for depth. It's the animation you were never going to hand-write for every CRUD list in your app, delivered in one line.&lt;/p&gt;

&lt;p&gt;That small surface matters for this story. It's why a complete Marko adapter fits in a ~45-line tag, and why the demo linked above covers the complete Marko-adapter surface in five routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The adapter's shape: why a tag
&lt;/h2&gt;

&lt;p&gt;Every framework adapter in this library mirrors its host framework's idiom. React and Preact get a hook that returns a ref. Vue and Angular get a directive you sprinkle on the parent element. Solid gets a directive too. Svelte uses actions.&lt;/p&gt;

&lt;p&gt;Marko has none of those primitives, and it turns out not to need them. The adapter is a tag with three attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;AutoAnimate
  parent=listRef      // a native element tag-variable
  options={ duration: 300, easing: "ease-in-out" }  // or a plugin function
  enabled=showMotion  // reactive boolean
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;parent&lt;/code&gt; is a Marko tag-variable: write &lt;code&gt;&amp;lt;ul/listRef&amp;gt;&lt;/code&gt; and pass &lt;code&gt;listRef&lt;/code&gt;. The target is explicit and typed, with no wrapper element and no DOM query.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;options&lt;/code&gt; takes the same object as the core (&lt;code&gt;duration&lt;/code&gt;, &lt;code&gt;easing&lt;/code&gt;) or a plugin function. It's read once when the tag mounts, matching the core's behavior (there is no &lt;code&gt;setOptions&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enabled&lt;/code&gt; is reactive. Flip it and the adapter calls the underlying controller's &lt;code&gt;enable()&lt;/code&gt;/&lt;code&gt;disable()&lt;/code&gt; for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest trade-off versus a directive: two touch points instead of one. You name the parent &lt;em&gt;and&lt;/em&gt; place a tag, where Vue users write a single &lt;code&gt;v-auto-animate&lt;/code&gt;. What you get back is more than the ceremony costs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reactive enable/disable as a plain attribute.&lt;/strong&gt; In hook-based adapters, runtime toggling means capturing a second return value and calling methods on it. In directive-based ones it's awkward or impossible. In Marko it's an attribute bound to any reactive value. The demo drives it with a two-way-bound checkbox (&lt;code&gt;checked:=enabled&lt;/code&gt;) and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;let/enabled = true&amp;gt;
&amp;lt;input type="checkbox" checked:=enabled/&amp;gt;

&amp;lt;ul/listRef&amp;gt; ... &amp;lt;/ul&amp;gt;
&amp;lt;AutoAnimate parent=listRef enabled=enabled/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Auto-discovery.&lt;/strong&gt; The package ships a &lt;code&gt;marko.json&lt;/code&gt;, so Marko's taglib discovery exposes &lt;code&gt;&amp;lt;auto-animate&amp;gt;&lt;/code&gt; in your templates without an import statement. The explicit &lt;code&gt;import AutoAnimate from "@formkit/auto-animate/marko"&lt;/code&gt; form works too, and is what the demo uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifecycle correctness for free.&lt;/strong&gt; The tag's &lt;code&gt;&amp;lt;lifecycle&amp;gt;&lt;/code&gt; owns setup, the reactive &lt;code&gt;enabled&lt;/code&gt; diff, and teardown (&lt;code&gt;controller.destroy()&lt;/code&gt; when the tag unmounts). Unmount the tag, or the branch containing it, and cleanup happens. There's no effect-ordering to reason about.&lt;/p&gt;

&lt;p&gt;And if you don't want the tag at all, the core function is one import away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client import autoAnimate from "@formkit/auto-animate"

&amp;lt;div/panelRef&amp;gt; ... &amp;lt;/div&amp;gt;
&amp;lt;lifecycle onMount() { autoAnimate(panelRef()); }/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same escape hatch every other framework's docs show; now Marko has it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it survive resume? (And why that question is different here)
&lt;/h2&gt;

&lt;p&gt;Let's be precise about what's special and what isn't. Server-rendering auto-animate is not a Marko-only trick. React under Next and Vue under Nuxt server-render lists fine, and after hydration a &lt;code&gt;useEffect&lt;/code&gt;/&lt;code&gt;onMounted&lt;/code&gt; attaches the animation. Same outcome. Qwik, which also resumes rather than hydrates, has an adapter too.&lt;/p&gt;

&lt;p&gt;What's different in Marko is the &lt;em&gt;mechanism&lt;/em&gt;, and it changes how the adapter must be built. Marko never re-renders on the client. There is no hydration pass that reconstructs the component tree and hands you a fresh ref. The HTML the server printed &lt;em&gt;is&lt;/em&gt; the app, and Marko resumes it in place. So the adapter can't hang off "the render that happens on the client," because there isn't one. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; and its rows arrive as real server HTML.&lt;/li&gt;
&lt;li&gt;On resume, the tag's &lt;code&gt;onMount&lt;/code&gt; runs (a client-only lifecycle phase).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;input.parent()&lt;/code&gt; dereferences the tag-variable, which points at the server-printed DOM node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autoAnimate()&lt;/code&gt; attaches its &lt;code&gt;MutationObserver&lt;/code&gt; to that node.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The subtle failure mode this design avoids: the adapter never depends on a sibling effect having run first, because the parent is a real DOM node reachable through the ref the moment resume reaches the tag. Marko's leaf-first resume ordering, a genuine trap for other integration shapes, simply doesn't apply here.&lt;/p&gt;

&lt;p&gt;You can verify the whole story in ten seconds: open the &lt;a href="https://codesandbox.io/p/github/defunkt-dev/auto-animate-marko-demo/main" rel="noopener noreferrer"&gt;demo&lt;/a&gt;, view source, and find the &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; rows sitting in the server HTML. No client render produced them; the adapter attached to them anyway. There's a passing SSR test suite upstream asserting exactly this, including plain object options after resume and an inline-template plugin case. Runtime plugin functions passed through server input are a different case and should be treated as client-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full API in five routes
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://codesandbox.io/p/github/defunkt-dev/auto-animate-marko-demo/main" rel="noopener noreferrer"&gt;demo&lt;/a&gt; is structured so each route teaches one thing, and the five together are the complete surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;/&lt;/strong&gt; covers add / remove / shuffle / reverse with the tag, plus the view-source SSR check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/options&lt;/strong&gt; covers &lt;code&gt;duration&lt;/code&gt; and &lt;code&gt;easing&lt;/code&gt;, shown as a side-by-side race: two identical lists mutated by the same click, one at 150ms, one at 1500ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/plugin&lt;/strong&gt; covers a custom plugin returning &lt;code&gt;KeyframeEffect&lt;/code&gt;s per action (&lt;code&gt;add&lt;/code&gt; / &lt;code&gt;remove&lt;/code&gt; / &lt;code&gt;remain&lt;/code&gt;) for a bounce-in, shrink-out, springy-slide feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/enabled&lt;/strong&gt; covers the reactive &lt;code&gt;enabled&lt;/code&gt; attribute on a checkbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;/dropdown&lt;/strong&gt; covers the raw-core escape hatch, no tag involved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One perceptual finding from building /options is worth passing on, because you'll hit it too: &lt;strong&gt;at the same duration, removals look slow and additions look instant.&lt;/strong&gt; That's not a bug and not the adapter. It's the library's default keyframes. A removed row is kept in the DOM as an absolutely-positioned ghost and fades out visibly for the full duration while its siblings slide up. An entering row never moves: it's held &lt;em&gt;completely invisible for the first half&lt;/em&gt; of its animation, then fades in (at 1.5x your configured duration, per the core source). A delayed fade with no motion reads as a pop, however long it runs. If you want loud entrances, that's what the plugin API is for. The /plugin route's bounce-in is the proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned shipping it: a Vite 8 gotcha
&lt;/h2&gt;

&lt;p&gt;The adapter merged, 0.10.0 published, the &lt;code&gt;./marko&lt;/code&gt; export went live. Then the first fresh &lt;code&gt;npm install&lt;/code&gt; into a new &lt;code&gt;@marko/run&lt;/code&gt; app crashed the dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[UNRESOLVED_IMPORT] Could not resolve '../index' in
node_modules/@formkit/auto-animate/tags/auto-animate.marko
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cause is a packaging subtlety. In the source repo, the tag imports the core as &lt;code&gt;"../index"&lt;/code&gt;, extensionless, which resolves to &lt;code&gt;src/index.ts&lt;/code&gt; and passes every test. The release build copies the tag into the tarball next to the compiled &lt;code&gt;index.mjs&lt;/code&gt;, keeping the import as-is. Older tooling would guess the extension. Vite 8's rolldown-based dependency optimizer applies strict ESM resolution inside a &lt;code&gt;type: module&lt;/code&gt; package and refuses to guess. Boom.&lt;/p&gt;

&lt;p&gt;No spelling of that import works in both places. Using &lt;code&gt;../index.mjs&lt;/code&gt; would break the repo's tests, and &lt;code&gt;../index.ts&lt;/code&gt; would break consumers. So the package-side fix needs to make the published .marko file resolvable under Vite 8’s dependency optimizer. Rewriting the import during the release copy step is one possible fix; excluding the package from optimizeDeps is the current consumer workaround. &lt;/p&gt;

&lt;p&gt;Until that lands upstream, the workaround is one config entry, which also reflects a general truth about &lt;code&gt;.marko&lt;/code&gt; taglib packages (they should be compiled by the Marko plugin, not prebundled):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;marko&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;optimizeDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@formkit/auto-animate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo ships with this and documents it. It's also a neat illustration of why "the tests pass" and "the published package works" are different claims: the upstream test suites import the tag from source, so no CI run ever exercises the installed tarball under a consumer's bundler. The only way to catch this class of bug is to do what you'd tell a user to do. Install the real release into a fresh app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install: &lt;code&gt;npm install @formkit/auto-animate&lt;/code&gt; (0.10.0+)&lt;/li&gt;
&lt;li&gt;Import: &lt;code&gt;@formkit/auto-animate/marko&lt;/code&gt;, or use &lt;code&gt;&amp;lt;auto-animate&amp;gt;&lt;/code&gt; via taglib auto-discovery&lt;/li&gt;
&lt;li&gt;Demo: &lt;a href="https://codesandbox.io/p/github/defunkt-dev/auto-animate-marko-demo/main" rel="noopener noreferrer"&gt;https://codesandbox.io/p/github/defunkt-dev/auto-animate-marko-demo/main&lt;/a&gt; · source: &lt;a href="https://github.com/defunkt-dev/auto-animate-marko-demo" rel="noopener noreferrer"&gt;https://github.com/defunkt-dev/auto-animate-marko-demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The adapter PR: &lt;a href="https://github.com/formkit/auto-animate/pull/239" rel="noopener noreferrer"&gt;formkit/auto-animate#239&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ship something with it, or hit an edge the five routes don't cover, I'd like to hear about it.&lt;/p&gt;

&lt;p&gt;by &lt;strong&gt;&lt;em&gt;defunkt-dev&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
