Skip to content

Changelog

Public and date-stamped, so you can check how often releases ship before you renew.

  1. 0.2.0 (unreleased), fix: what the audit of every product found

    • An audit of every product on 2026-09-27 held each component to what its page and its docs promise, in the browser. This release fixes what it found that needs no decision from the owner; the rest waits for one.
    • Overlays. Nothing inside an open Drawer took a mouse or pen click, because the sheet captured the pointer on every press; a press becomes a drag after 6 px now. In a browser without invoker commands, VideoDialog played its video, sound included, behind a dialog that stayed closed. CommandPalette keeps its list still under the arrow keys, closes on a click on its backdrop in Safari, which has no closedby, and announces how many results it found. A morphing Dialog no longer fades and scales like a plain one, and its close button sits at the inline end on a right-to-left page. Toasts are announced from a region that exists from page load, and their clock stops on hover, on focus and in a hidden tab. LinkPreview's card can be hovered, as WCAG 1.4.13 asks.
    • Buttons and inputs. Ripple drew its rings beside the element in every engine. StickyBanner's remember reopened the banner after a ClientRouter navigation. StatefulButton was named by all of its faces at once; it is named by the one you see. ThemeToggle changes the page on the first click when its choices leave out system, shows the right icon from the first frame and stays hidden without ThemeScript. PriceSwitch shows the price of the chosen period in browsers without round() and mod(). CopyButton no longer stays on "Copied" after a transition:persist swap. A style prop no longer replaces CountUp's duration or Progress's variables. OtpInput takes an id of its own per field and keeps its digits in order on a right-to-left page.
    • Pointer, media and layout. Cursor gives the native pointer back when reduced motion turns on. AnimatedBeam draws one packet of light per path, as documented, where it drew dashes. FilterGrid keeps a filtered-out card hidden whatever display the card sets. Magnetic no longer takes the clicks of the elements beside it. HoverVideo is a link that plays on keyboard focus. ImageCompare, AvatarStack, ArrowButton and FileTree follow a right-to-left page. AnimatedList says to prepend a live item, since an appended one never shows in a full list, and a list too short to cycle stands still.
    • Underneath. Globe, Particles and Confetti kept a frozen canvas after their tab was hidden and shown again. After a navigation from a page that loads no Moonarc script, html[data-ma-js] was gone for the rest of the visit, so the components that wait for JavaScript showed their content at once; the head gate sets it again after every swap. A component whose setup throws no longer stops the rest of the page from binding. The reduced-motion switch also stops Astro's route transitions now, and mirrors media query lists and combined queries. With Tailwind, duration-ma and ease-ma resolve, so a data-ma-preset subtree retunes the utilities too. A library view transition no longer captures every element named with transition:name. An element you take out of the page is released by the next refresh(), with its listeners and its GPU context.
    • Pro sections. MegaMenu's panels were unreadable on the ink and brand bars; they keep the popover's colours on every tone, and the triggers reset the native button. NewsletterSection can post to Mailchimp or ConvertKit: a name prop for the field, a honeypot, formAttrs, a slot for hidden fields, and post only. ChangelogSection's link sits under the items on a phone. FeatureAtlas no longer hides a live demo slot from screen readers. LogoWall's height prop sets the logo height in both layouts. StepsSection is a valid list, and its line and FeatureRows' copy follow a right-to-left page. The fixed English labels of six sections are props, and 27 of the 28 usage examples start with the import path the registry writes.
    • On this site. A Pro page listed related sections and effects as free; it lists the primitives a section composes, or an effect's free relatives, and its Markdown twin says the same. A GPU effect's readout prints what it loads with its GL libs, where it printed its own script alone. A free component page says what its registry item installs and which files a paste needs, where every page said the runtime was its only import. The /theme preview shows sample plans.
  2. 0.2.0 (unreleased), fix: what the component audit found

    • CountUp read wrong in Safari and every WebKit browser: each group of digits showed the first group's value, so 2,847 read 2,002. The counter is now reset on the pseudo-element that prints it.
    • Spotlight measured the pointer in screen pixels and painted in its own, so under a scaled ancestor the glow sat at half the distance. AvatarStack's count grows from a circle into a pill when it is wider ("+2,398"), and keeps its circle in a project without a CSS reset.
    • PricingTable: an empty cell says none, and screen readers still hear "Not included". When a narrow screen cuts the table, the section scrolls sideways with a tab stop, a name (the new label prop, "Plans compared" by default) and a focus ring on its border in the tone's ring colour.
    • The usage examples of CompareTable, PricingTable, PricingCards, StatsRow, Progress, MegaMenu, HeroTerminal, HeroEditorial, AnimatedList and StickyBanner use sample data where they typed Moonarc's own prices, counts, bytes or releases. The demos on this site read theirs from the store, the catalogue and this changelog, and label the rest as samples.
    • The Pro registry answers a key check that got no answer (a rate limit, an outage, a timeout, a misconfigured site) with 503 and Retry-After. It used to answer 401 with the upgrade prompt, which told a paying buyer to buy Pro. Keys are checked through Polar's authenticated API, and the requests npx shadcn add makes at once share one check.
  3. 0.2.0 (unreleased): the site measures before it prints, and an About page

    • The live site printed "not measured" on every share card and readout, and counted all 102 primitives as zero bytes: its production build ran without the budget report. The production build now measures the component fixtures and then the pages it prints, and stops when a figure is missing. The runtime reads 1.9 kB raw everywhere; six places had it typed as 1.8 kB raw.
    • Claims about other libraries name their source or state our measurement, with versions and browsers, or are gone. React's cost is the one /compare/react/ ships, React 19.3, where the site had printed React 18's size. Three counts of primitives that sounded alike have one phrase each: "load no JavaScript", "no script of their own" and "never load the runtime".
    • lenisPage: a link clicked, or the back button pressed, while Lenis was still easing a wheel scroll left the next page where that scroll was heading. The resync stops the animation first and then restores the position the router set. lib/lenis is 986 B raw, under its 1 000 B raw ceiling.
    • The gated Pro registry answered 500 to every request in production, with or without a key: it read its files from a disk the server does not have. It serves them from the build now.
    • /about/ says who builds Moonarc, why, how it is tested and paid for, and what keeps working if it stops; /about.md is the same page for agents.
  4. 0.2.0 (unreleased): the library is Moonarc

    • The library is called Moonarc and lives at moonarc.dev. Install it with npx astro add moonarc; the packages are @moonarc/core, @moonarc/mcp and @moonarc/pro.
    • moonarc@0.0.1 on npm is a name-claiming placeholder. The packages publish at 0.1.0; @moonarc/core and @moonarc/mcp are not on npm yet.
    • Every custom property, class, data attribute, event, window global and environment variable the library defines starts with ma now: --ma-brand, .ma-sr, data-ma-theme, ma:form-done, MA_FORM_DEMO. Both prefixes are two letters, so no raw size grew. The runtime, and every figure measured with it, lost 5 B raw, because the tag on its error log is the new, shorter name; gzip sizes shifted by a few bytes either way.
    • The shadcn registry namespaces are @moonarc and @moonarc-pro. Items install into src/components/moonarc/ and src/lib/moonarc/, and the two stylesheets are moonarc.css and moonarc-theme.css.
    • The MCP server reads a Pro key from MOONARC_KEY.
    • npx -y @moonarc/mcp starts the MCP server, and npx @moonarc/mcp audit src runs the audit in CI. Before, npx stopped with "could not determine executable to run": the package had two commands in two files and neither carried its name, so npx could not choose. Both commands now run one file, which starts the server when an agent launches it and runs audit or measure when you name one.
    • The integration package and the MCP server now ship the MIT licence text with them.
  5. 0.2.0 (unreleased): the home stops carrying 141 demos

    • The home page ships 177 009 B raw of CSS where it shipped 410 361 B raw, and 38 598 B gzip where it shipped 74 643 B gzip. Its JavaScript is byte-identical. The CSS of /pro went from 407 423 B raw to 267 196 B raw, and from 73 860 B gzip to 53 065 B gzip.
    • What was wrong: the catalogue card looked its demo up in a glob of the whole stages folder. A demo's CSS follows the module that imports it, and Astro gives that CSS to every page that can reach the module, so a page with one card carried the styling of all 141 demos. The card takes its demo from the page now, and the home imports the twenty-one it shows, free and Pro, with a build that fails if a tile names a stage nobody imported. The catalogue, a category page and a component page ask for the whole shelf on purpose, through one module that says so.
    • A non-eager glob in the card looks like the answer and is not: attribution walks dynamic importers too, so every page still reached every stage, and splitting one file into 140 small ones cost 9 kB gzip on the home. The mechanism content collections use to attach styles per rendered page does not survive the compiler on a component with a script. Both measurements are in docs/stage-css.md.
    • A component page still carries every demo, because one module renders all 141 of them and CSS is attributed per module. The file is shared, so a reader who opens a second component page pays nothing more for it.
  6. 0.2.0 (unreleased), fix: the filter and the adjust bar after a navigation

    • Every component binds on the swap that puts the page on screen, and no longer waits for the load event that follows it. Astro's router fires astro:page-load from the promise that waits for the new page's scripts, and a navigation interrupted after its swap leaves that promise unsettled, so the view on screen never gets one; a slow script request delays it for as long as the request takes. The runtime now binds on astro:after-swap as well, which fires once on every swap, and one registration that throws is reported and stepped over instead of ending the pass and leaving every later component on that page dead until a reload. 1 828 B raw to 1 909 B raw.
    • What this looked like on the site: the catalogue filter vanished and the Adjust bar on a component page arrived only after a reload. Both ship invisible until their script reveals them, so a bind that never ran read as a missing feature.
    • The catalogue filter also ships open now. It is a <details> whose summary is hidden from 64rem up, so a box still collapsed from the phone layout became a filter with no way back the moment the window was widened past the breakpoint. Open is what the server sends, an inline script collapses it on phones before first paint, and the crossing is covered by a media query listener with a ::details-content rule under it.
    • The site's own chrome follows the advice the site gives for yours: the reduced-motion toggle in the header and the runtime readout on /playground bind through onMount and onPage rather than their own astro:page-load listener, and GlyphField's inlined swish listener rebinds on the swap too.
    • The browser gate drops every astro:page-load listener on the page and then walks the catalogue and two component pages twice each, because the first visit to a page runs its module and hides the bug; it also widens a phone-width catalogue past the breakpoint. All three checks were run red against the old code first, in all three engines. Written up in docs/lifecycle-audit.md.
  7. 0.2.0 (unreleased), fix: typing that arrives in Safari

    • Typewriter and HeroTerminal type all the way through in Safari. Each character, and each line a terminal prints, is now revealed by an interpolated keyframe, which every engine lays out whenever it lands.
    • What was wrong: a stepped keyframe that lands more than 1 000 ms into a page reaches computed style in WebKit 26.6 but never layout. The home hero had been showing 2 of its 26 characters, with the caret 464 px to the right of them, and a terminal printed none of its three output lines. Chromium does the same for every property except display; Firefox lays out every case. Animations that loop, and stepped ones with a long active interval, were never affected. The browser gate now reads the laid-out box on the home hero and on a terminal, in whichever engine it is pointed at, and both checks were run red against the old CSS first.
  8. 0.2.0 (unreleased), phase 7F.3: the default palette

    • A new default palette in theme.css, from one brand colour. Cerulean #2978a0 is --ma-brand; a tinted neutral (--ma-neutral, Jet #253031) and an accent (--ma-accent, Sand #bcab79) are derived from it, and a neutral scale, --ma-neutral-50 to 950, is what the page, the text, the borders and the fields point at. Set --ma-neutral or --ma-accent to choose your own: the builder at /theme has an input for each.
    • The painted tones are theme scopes. A section with tone="muted", "ink" or "brand" re-declares the card, border, field, focus ring and elevation tokens inside it, so its cards, hairlines and fields follow the ground. The brand tone has a ground of its own, --ma-tone-brand: the brand colour at lightness 0.46 in both schemes. The tone attribute is now data-ma-tone.
    • Status colours: --ma-success, --ma-warning and --ma-info, each with a -foreground, and new values for shadcn's --destructive. Each reads at 4.5 : 1 or more as text on the page and under its own text. With the Tailwind bridge they are text-success, bg-warning and bg-info.
    • With theme.css, secondary text is never fainter than 0.75 opacity (0.8 on muted, 0.9 on brand, including the brand panel CtaSection and HeroField paint when no tone is set), so a pasted theme with a gentler text colour still reads at 4.5 : 1. The Waitlist template no longer patches this itself.
    • BlogGrid shows each post as a card. The light --primary is the brand's 600 step (it was 700). The registry's theme item carries the seeds and the roles and leaves every derived value to theme.css, so a later --ma-brand recolours the scales.
    • FeatureAtlas keeps to one column under 40rem: its wide cell no longer adds a second column beside the others.
  9. 0.2.0 (unreleased), phase 7G: the template foundation

    • Two Pro sections, 28 in all, and both of them are the waitlist: HeroWaitlist (a RetroGrid floor under a plate, CountdownFlip to the launch date, a VanishInput email field, the consent CheckDraw, an AvatarStack of proof and a ShimmerButton that submits, 43 B raw of its own) and ThankYou (the page the form leaves you on: the position read back from the record the form lib wrote, never from the URL, a referral link with a CopyButton, and one Confetti burst that never fires twice, never on a reload and never in reduced motion; 981 B raw, and the none variant ships no canvas at all). Five axes, named slots, and no tone unless you set one, so neither paints a ground it was not given.
    • @moonarc/core/form: the send path, in core and MIT, because a form that arrives is not a paid feature. One listener over every <form data-ma-form> encodes as JSON, form-data or url-encoded for the adapter (demo, Formspree, Netlify, or your own endpoint), locks the form with data-pending and aria-disabled rather than disabled, so focus is never dropped from under the pointer, writes its status into a role="status" node outside the form, waits for the field to finish vanishing before it leaves, carries the answer to the thanks page in sessionStorage and dispatches ma:form-done or ma:form-error exactly once. 3.0 kB raw; 4 789 B raw on its fixture page with the runtime under it. With JavaScript off the same markup is a plain form and posts natively (the server's 303 lands on the thanks page), and every path is checked in three engines, with the tab hidden, offline, across a ClientRouter swap and back through bfcache. Written up at /docs/forms/ and served as a registry:lib item at /r/form.json.
    • templates/: the tree the products live in. templates/kit is what every template is built on: Layout, Head, Analytics and LanguagePicker, plus modules for the site config (zod-checked, and a bad colour now fails when the config loads and names the key instead of halfway through a share card), forms, robots.txt, 1200 × 630 share cards rendered at build, i18n, RSS and Pagefind search. templates/waitlist is the first template: brand Lanternfold, three pages with every string in content and none written into the markup, the theme pasted out of /theme, the logo and the five avatars drawn here, nothing third-party. Each page's JavaScript under a ceiling of its own, which the template gate measures; 49 links, no violations across four passes and two states, and 26 text-on-ground boxes at or over 4.5 : 1 under the shipped theme and two pasted third-party ones. templates/fixture is the other half: a template nobody buys, whose whole job is to exercise what Waitlist does not use: all four form adapters, MDX posts with a feed, search, two locales and a ClientRouter page.
    • templates/release.mjs writes what the buyer actually gets, and tests/templates proves it is the same project as the demo. The copy carries the template file for file, with Pro and the kit vendored under vendor/ as file: dependencies (only what the import walk reaches: six Pro components and no lib, each package with its licence and the fonts with their OFL), and a package.json that may differ from the repo's in its dependency specifiers and in nothing else, diffed line by line before the copy is allowed out: 56 files, 519.6 kB raw, and a zip that is byte-identical written twice. The gate builds and audits the workspace copy, then installs the release copy outside the workspace with npm and again with pnpm (the two resolve a file: directory differently), and runs a11y, links, the form flow, SEO and the ceilings over both; then rebrands that copy through src/config, src/content, src/assets and public alone, holds the diff against that rule before anything is built, and checks that the demo adapter is still refused without its opt-in. The CSS of all five builds is byte-identical, which is how we see that Tailwind never scanned vendor/. The site gates can be aimed at another build for it: a11y and links take --dist, and with it links also reads form actions and every srcset.
    • Fixed underneath, in the primitives a template leans on. VanishInput had one id per field name, so two forms on a page shared it, and its hint and placeholder sat at 2.52 : 1. They take their colour through the --ma-dim scalar now and clear 6.4 : 1 in light and 7 : 1 in dark in all three engines. CountdownFlip hid its own done text and never spoke it, said "1 days", clipped a three-digit day count, and had shown two digits at once after every flip since it shipped (the flap is a translucent tint, so the old bottom read through it). CopyButton was named by all three of its faces at once while its aria-live announced nothing: the inactive faces leave the accessibility tree and the status is a role="status" node beside the button. Confetti handed var() straight to fillStyle, so the site had been dropping black confetti, and it takes trigger="event" now, fired from your own code without importing the lib. FooterSection rendered an empty <p>. And ThemeToggle bound itself to [data-ma-theme], which since 7F is also the theme-scope attribute, so a click anywhere inside a section demo cycled the site theme; it binds button.ma-theme now, the same fifteen characters, and the home page is byte-identical at 43 512 B raw.
  10. 0.2.0 (unreleased): GPU effects in Pro

    • Ten more GPU effects, 11 in Pro: LiquidImage (a real <img> that turns liquid under the cursor and settles back), Dither (Bayer-dithered waves in pixel cells), Silk (lit fabric folds), Plasma (glowing ribbons), LiquidChrome (a pool of liquid metal), Gooey (metaballs that follow the pointer), Hyperspeed (light streaks; hold to punch the drive), FlowParticles (GPGPU particles on curl noise with trails), InkReveal (ink washes a GPU cover off real content) and WaveField (the wave equation in halftone, with rain). Each is tier C on its own script, demoed and tuned on its page, and checked for lost contexts, the motion switch and navigation leaks.
    • One GL core under all of them, extracted from FluidField with its measured behaviour: lib/gl (the context lifecycle: made the first time the effect is on screen, restored after a GPU reset, released on navigation; plus passes, float targets and pointer deltas), lib/fluid (stable fluids with walls) and lib/shader (the runner for one-pass effects). They are Pro code: shipped as files of each gated registry item, never as a public /r/ item. FluidField's own script went from 8.4 kB raw to 1.9 kB raw.
    • Home hero: the second background is WaveField (ripples on the GPU, a wake behind the cursor and rain while it rests) in place of the glyph field, tuned from the same strip. Reaction–diffusion was tried for the slot and filled the box within seconds. The home ceiling moved from 41 984 B raw to 43 520 B raw for it; the composition is printed in the budget file.
    • /pro now opens on the home's hero layout running what it sells: FlowParticles, with fluid, waves, silk and dither on the strip, each with its readout, cost and page.
  11. 0.2.0 (unreleased): FluidField and the new home hero

    • Pro now has two kinds: sections, and GPU effects: WebGL primitives priced as Pro because they are the expensive work, demoed and tuned on their page like any primitive. FluidField is the first and moved from core to Pro (@moonarc/pro/FluidField, registry @moonarc-pro/fluid-field); it never shipped in a core release.
    • FluidField (Pro): smoke that follows the cursor, from a real fluid simulation on the GPU (WebGL2: advection, vorticity confinement, a pressure solve) in two tones, the ground and one colour. Velocity on a 128-cell grid, the dye on 512 and in one half-float channel, so the colour is applied at display time and follows the theme without resetting the smoke; dots draws it as a halftone. A slow emitter wanders the focus while the pointer is still and on touch; every constant is a custom property, re-read when the style changes. The loop runs only on screen in a visible tab, the context is released on every swap, reduced motion draws one settled still, and without WebGL2 float targets a CSS glow stands in. 7.4 kB raw, above tier C by decision, stated in budgets.json.
    • Home hero rebuilt: the ink panel is gone. The copy sits on the ground with a scrim, the halftone FluidField in the site accent runs behind it by default, and a strip at the bottom prints what is running and what it costs, switches the background between the fluid and the glyph field (hidden, so the other costs nothing), and opens tune for whichever is showing. The choice lives in the URL. The home JS ceiling moved to make room for the fluid.
  12. 0.2.0 (unreleased), phase 7F: the theme layer

    • The 26 Pro sections now read the shadcn variable names (--background, --card, --primary, --border, --radius and the rest) with the base tokens as fallback (var(--card, var(--ma-panel))), so a pasted shadcn or tweakcn theme recolours every section without editing one, and a page with no theme renders exactly as before: measured pixel by pixel on every section page, dark and light, desktop and mobile, zero changed. No radius, section padding or colour is hard-coded in a section any more.
    • @moonarc/core/theme.css: the contract with its default light and dark values, --ma-brand with a scale derived in CSS (oklch(from …)) over a static ramp for browsers without it, --ma-tone-ink for the always-dark panel shadcn has no name for, a radius scale on --radius, --ma-density, fonts and shadows. Every rule is :where(), so a consumer's :root or .dark wins; a data-ma-theme subtree carries a theme of its own. The Tailwind bridge (@theme inline, bg-background, rounded-lg on --radius) is a second, optional file.
    • Five axes on the sections, each a prop and the data-* attribute it writes: tone (surface · muted · ink · brand; the primary action inverts on a painted ground), density (compact · cozy · roomy through --ma-density), align, columns and media. Named slots (eyebrow, headline, copy, actions, media, aside) on every section that has the part; a slot wins over the prop. Each section page lists its classes and slots and has a variants panel that writes the attributes on the running demo.
    • The theme builder at /theme: one colour, a radius, a density, fonts and a motion preset; six real sections follow at once in a data-ma-theme scope; the CSS to paste, with the brand scale precomputed under @supports not for browsers without relative colour; the contrast of every changed pair; the choice in the URL. The registry serves the default theme as a registry:theme item (npx shadcn add …/r/theme.json).
    • Contrast is checked by a script. The scale promises white text on the 700 step and near-black on the 400 step at full and 0.75 opacity for every hue up to chroma 0.28, the ring 3 : 1, adjacent chart colours ΔEok ≥ 0.09 in colour and under a deuteranopia simulation; check-contrast.mjs proves every pair a section paints in both default themes and per tone. Two things it changed: --primary is the 700 step, not 600 (0.75-opacity copy needs L ≤ 0.44), and the brand tone raises its faintest text to 0.8 opacity (a 0.6 kicker never passes on a colour lighter than L 0.38).
    • Site: --muted and --accent were renamed to --muted-fg and --signal: both are surfaces in the contract and were text colours here; the contract is aliased in global.css to what the sections painted before, so the demos are unchanged. Sparkline and BarGrow default to --chart-1 when a theme defines it. Browser support: relative colour and contrast-color() added to the audit.
  13. 0.2.0 (unreleased), phase 7E: input, loader, magnet

    • Eleven primitives, ten at zero bytes. Six of them are form controls that stay native: Switch (a checkbox with role="switch" and appearance: none; the input is the track, its ::before the thumb on the preset spring), CheckDraw (a checkbox whose tick is a pathLength-1 stroke drawn on :checked, a bar on indeterminate), OtpInput (one input, because paste, SMS autofill and Backspace work only that way, drawn as cells by a computed letter-spacing; the check draws when full and valid), AnimatedSelect (a native select; where appearance: base-select exists the picker takes the tokens and the options stagger by sibling-index(), elsewhere the styled native control), PromptInput (a form with a field-sizing textarea, a model select and a real file input; submitOnEnter is a 327 B raw variant that sends on Enter, breaks the line on Shift+Enter and never interrupts an IME), VanishInput (hints take turns through one registered number; a submit the host prevented splits the value into characters that blur away, 604 B raw), AvatarStack (overlapping avatars that spread on hover, names as CSS tooltips, no tab stops), CounterPreloader (0 → 100 through counter-set on a fixed layer that never takes the pointer; once inlines 187 B raw and re-runs after every swap), DynamicIsland (every state declares its size; the shape transitions on the spring, the content cross-fades), PhoneFrame (a generic phone in container units, with an island, notch or punch and no brand, and a screen that drifts) and LiquidGlass (glass in every engine; the rim refraction is an SVG displacement map on its own backdrop-filter layer, painted by Chromium and by nothing else, measured).
    • Catalogue tiles pause when they are more than a viewport away: one IntersectionObserver owned by the site (never the home page) marks the stage data-stage-off and animation-play-state: paused takes every animation under it, since content-visibility: auto skips style, layout and paint but not animations (measured in 7D). Stage scripts skip their ticks through the same attribute. Idle style work in the middle of the catalogue fell by a third; see docs/phase-7e.md for the table.
    • Fixed: the catalogue filter nulled ClientRouter's history state when it wrote the URL, so back from a component page changed the address and left the page, and a dev-server reload landed at the top. It now keeps the router's state, remembers the query in sessionStorage, and the "Components" breadcrumb on a component page returns to the filtered list.
    • Fixed: CommandPalette scrolled the whole page to itself whenever it rendered results off screen: scrollIntoView() scrolls every ancestor, and the catalogue tile types on a timer. The list now moves its own scrollTop only. On the catalogue this was the page jumping while you scrolled.
    • Browser support: :user-valid / :user-invalid, appearance: base-select with ::picker(select) and ::checkmark, sibling-index(), field-sizing and the Safari switch attribute added to the audit from BCD 8.1.1; backdrop-filter: url(#svg) measured in three engines (Chromium paints it, Firefox and WebKit accept the value and paint nothing on that layer). Found: a lightningcss build with both backdrop-filter and -webkit-backdrop-filter written keeps only the prefix; a node kept through transition:persist restarts its CSS animations; a <style> literal in a component doc comment confuses the budget harness.
  14. 0.2.0 (unreleased), phase 7D: navigation, overlays, galleries, layout

    • Fourteen primitives, six at zero bytes: PillNav (the active pill is a named element the router slides across pages; the hover pill is anchor positioning), Dock (the tile under the pointer swells and its neighbours with it: one size number set by :has(), width and height on the preset curve), StaggerMenu (a modal dialog on the invoker, items delayed by index through @starting-style and reversed on the way out, the X folding out of the burger), CommandPalette (⌘K: a native dialog, a stated four-level ranking, ↑↓ ↵, aria-activedescendant, one fetch of a JSON source; 2.4 kB raw), StickyBanner (a non-modal dialog closed by a form; the fold is a grid row on :has(); remember inlines 286 B raw), LinkPreview (a manual popover anchored to the link whose hover intent is a CSS transition-delay, 656 B raw), Lightbox (thumbnails that morph open into the full image on Dialog morph, ← → through the set, 972 B raw), ImageCompare (a native range over the box, one property, 205 B raw), HoverVideo (play on hover, rewind on leave, preload none, a button on touch, 666 B raw), VideoDialog (an invoker-opened player that stops when the dialog closes, because a closed dialog stops nothing (measured); 518 B raw), AnimatedList (a pure-CSS notification cycle: one registered number per item and calc() on its time slot), FilterGrid (a same-document view transition with match-element names, 987 B raw), FileTree (nested details with ::details-content rows) and HoverImageLinks (one floating box per list, the pointer's lengths and lean as custom properties, 616 B raw).
    • Two props: Tabs direction (the panels are one track translated by the checked index, so the slide is toward the selection with no memory of the previous tab) and Dialog morph (the box grows out of its trigger on a same-document view transition and returns to it: an internal component rendered only when the prop is set, measured on its own fixture at 522 B raw; the default stays at 0 B).
    • lib/morph (1.1 kB raw on its own fixture): startViewTransition with html[data-ma-vt] for the duration, the reduced-motion and no-support fallbacks, and morphOpen / morphClose for dialogs. base.css gives the root a plain cross-fade on the tokens under that attribute, so a Curtain on the same page never opens its iris for a morph, and elements named by Astro's transition:name step out of a library transition.
    • Catalogue cards: click, pointer and hover stages now sit above the card's stretched link, so Tilt, Spotlight, Magnetic, Cursor, Globe and the new hover components can be tried on the card; the title still opens the page and the card → page morph is unchanged.
    • The invoker polyfill (Dialog, Drawer, StaggerMenu) now honours defaultPrevented, so a morphing dialog on a browser without command / commandfor opens once instead of twice (+20 B raw on the home page, 23 B raw of headroom left). The site keeps its own Search palette: the primitive is 913 B raw larger.
    • Browser support: view-transition-name: match-element, vertical range inputs, animatable grid-template-rows and interestfor (Chrome-only, the proposed zero-byte route for LinkPreview) added to the audit. Measured in three engines: a lazy iframe inside a closed dialog does not load; a playing video inside a closed dialog keeps playing.
  15. 0.2.0 (unreleased), phase 7C: pointer, backgrounds, canvas

    • Twelve primitives, eight at zero bytes: Cursor (a ring or a "View" pill trailing the pointer: the lag is a CSS transition, the targets one delegated listener, 1.0 kB raw; not installed on touch), ImageTrail (a server-rendered pool of lazy images popped along the pointer's path every N px, 875 B raw), Confetti (a burst on a fixed canvas that exists only while pieces fall; physics as custom properties per preset; also confetti() from your own handler), RetroGrid (a perspective floor translating one cell on the compositor), AnimatedBeam (packets of light along server-computed curves between slotted nodes (hub, row, fan), nothing measured), OrbitingCircles (one transform keyframe per item, count from :has()), Orb (three gradients breathing, one registered angle turning every stop's hue), ProgressiveBlur (bands of doubling backdrop blur under overlapping masks), DottedMap (the world from a 1° Natural Earth mask as one dash-dotted path, 5.9 kB raw, no script), Particles (a drifting field that parts around the cursor, 1.6 kB raw), Meteors (seeded streaks on a twinkling sky, the same seed the same sky) and Globe (dots on a turning sphere from a 1.2 kB raw mask, three batched fills a frame, draggable with inertia, 2.8 kB raw).
    • The first canvas components come with a shared helper, @moonarc/core/canvas (1.7 kB raw, measured on its own fixture): a ResizeObserver fit with the pixel ratio capped at 2, a frame loop that runs only on screen and in a visible tab, and a theme observer that re-reads the colours. The tokens reach the context through the canvas element's computed colour, so oklch and color-mix() arrive as rgb.
    • Land data: packages/core/scripts/build-land.mjs rasterises Natural Earth 110m land (public domain) into a 1° bitmask, imported on the server only; DottedMap draws from it, Globe ships a downsampled copy as JSON in its HTML.
    • tests/consumer is green again across Astro 5, 6 and 7 in static and server output: the Aurora check named keyframes from before phase 6, and the Typewriter check read the first character in the one frame its 1 ms step-end animation shows the from keyframe. It was a race in the test, proven with a per-frame probe on the server build; it now waits for the condition.
    • Browser support: backdrop-filter added to the audit (Chrome 76 · Firefox 103 · Safari 18); ProgressiveBlur states its gradient fallback.
  16. 0.2.0 (unreleased), phase 7B: buttons, frames, scroll

    • Thirteen primitives, ten at zero bytes: ShimmerButton (a sheen on a loop, lift and ring on hover and focus), StatefulButton (idle → pending → done → error in one button whose width follows the label with nothing measured; a form's data-pending drives it), ArrowButton (the arrow leaves and the next one arrives, in four directions), CopyButton (the clipboard write is the only script (584 B raw); the check and the label are CSS; a refusal says "Copy failed"), ThemeToggle + ThemeScript (system → light → dark through a circle from the click on a same-document view transition; the head script is the anti-flash half, 473 B raw inline; 1.0 kB raw for the toggle, which is inert without it), AnimatedIcons (menu, plus, copy, play, sun, check; each stroke its own path; play/pause morphs with d: path() where it is a CSS property and cross-fades in Safari), BrowserFrame (chrome in container units, a pinned theme redefines the tokens locally), CodeTabs (Shiki at build with both themes as variables and --ma-dark picking one; radios + :has(); CopyButton per panel), StickyReveal (each step names a view timeline; the sticky visuals read them through timeline-scope; a container query and display: contents interleave them elsewhere), ScrollExpand (a clip-path inset with its rounding on the element's own view timeline), TracingBeam (one registered percentage that both the fill and the head read), FooterReveal (position: sticky under the content: no animation, nothing to reduce) and ReadingToc (one IntersectionObserver writing aria-current, 987 B raw; the indicator is CSS).
    • Two props: ScrollScrub rotateX + perspective (the container-scroll tilt, on the element itself) and Spotlight target="border" (the light kept to the card's edge by BorderBeam's two-mask ring, zero extra bytes).
    • The site's header theme toggle is the library's ThemeToggle and the anti-flash script is ThemeScript; the local component is gone. Install sections use CodeTabs (npm / pnpm / bun). Code blocks on the site are now actually highlighted: Shiki had been emitting both themes as variables and no rule picked one.
    • Firefox and WebKit, finally: every 7A component and every 7B one checked in three engines (tests/site/browser.mjs --engine all). Found and fixed: WebKit painted a stale glyph on SplitFlap and GlyphField when a counter-reset value changed through a transition, and counter-set repaints correctly in every engine; Firefox rejected a unitless calc() on stroke-dashoffset, so Progress's ring never filled there; it is × 1px now. Curtain's eight modes compute their clip and mask on the root snapshots in all three (Playwright's Firefox screenshot does not capture a view transition mid-flight, so the check reads the pseudo-elements' computed style instead).
    • Browser support: counter-set, timeline-scope, d: path(), the clipboard API and interpolate-size added to the audit. interpolate-size is listed and unused: it does not animate auto → auto, so StatefulButton bounds its width from the label length instead.
  17. 0.2.0 (unreleased), phase 7A: transitions, text, numbers

    • Fifteen primitives, thirteen at zero bytes: SplitFlap (a departures board: every letter a CSS counter on a registered integer, a flap per step), TextRoll, CircularText, GlitchText, StrokeDraw (SVG text, one stroked glyph at a time), HighlightMarker, UnderlineHover (in from the left, out to the right), BubbleText, NumberFlow (digits roll to a value written as one custom property; round() and mod() do the digits), PriceSwitch (two radios + :has() driving NumberFlow, the badge through @starting-style), CountdownFlip (one interval a second, the fold is CSS, 1.1 kB raw), Sparkline and BarGrow (server-rendered SVG and bars that draw and grow on scroll), Curtain (a full-viewport wipe between pages on the root view-transition snapshots (iris from the click, blinds, shutter, doors, pixels, stagger, diagonal, title), 479 B raw for the click point) and PersistPlayer (an <audio> element that rides into the next page under transition:persist).
    • A tenth category, data, for the numbers and charts; CountUp and Progress stay where they are.
    • Six props on existing primitives: SplitText mask (each part rises from behind its own line) and scrub (words light up on the block's view() timeline), RotatingText fluid (the box follows the current word), Reveal from="clip" (a wipe from the bottom edge; it is a mask because Chromium's IntersectionObserver applies the target's clip-path), Progress shape="ring", PageTransition preset="scope".
    • Catalogue tiles are skipped while off-screen (content-visibility: auto on the card stage): with fifteen more live tiles the catalogue page idles at 18 ms/s of style work against 31 before, and layout at the top is gone. Stage boxes clip instead of hiding overflow, so a view() timeline inside a stage runs against the page again.
    • The demo pages carry a Curtain, a persistent player and a mode picker; the two pages are coloured differently so the shape between them shows, and a named PageTransition region keeps its own preset through the same swap.
    • Browser support: clip-path basic shapes and mask-composite added to the audit, round() joins abs()/mod(). Verified in Chromium only: Firefox and WebKit are not in this machine's Playwright cache; every fallback is stated on the card.
  18. 0.2.0 (unreleased): the glyph field

    • GlyphField: a field of braille dots (ascii and block sets as a prop) as CSS counters on registered integers: contours from server-side noise, two waves stepping one inherited integer per row (mod() and abs() fold each cell's crest), colour per cell along a sunset arc, a hover trail that decays one character at a time. 0 B; the swish prop inlines one measured pointer listener for a two-dimensional wake with a tail.
    • Home hero rebuilt on GlyphField, motion.dev's canvas as the reference and the brief "wilder, not a copy": kicker with the live counts, an amber first word, availability chips, the readout printing the measured swish bytes, and a tune panel (palette, glyph set, wave, reach, fade, hue) that writes custom properties on the running field.
    • HeroField (Pro) now composes GlyphField and takes available, hue and swish.
    • Fixed: RotatingText never showed a word on a page that also had CountUp, and Tabs, Typewriter and Spotlight had the same latent collision. @property registrations are global, and a short name (--ma-n, --ma-y) registered by one component as non-inherited zeroed it for another that read it through inheritance. Every custom property is now namespaced per component (--ma-rotate-*, --ma-count-*, --ma-tabs-n, --ma-type-n, --ma-spot-*).
    • Full audit: every route swept at desktop and mobile for errors, overflow, unlabelled controls, heading outline and stale copy. The type scale is now the measured pixel scale (44/40/23/15/13/11; rem values on a 15px root had left every size 15/16 short, the kicker at 10.3px). Hero sections, the 404 and the docs shell take a level prop so a demo never adds a second h1. A 404 page built from NotFoundSection. Component pages fold the full source to a screen and a half with a zero-JavaScript expand. The search field has a name. The home prints the latest changelog entry from one shared list; every remaining typed count reads from the catalogue.
    • Pro page rebuilt from the Pro sections themselves (HeroField, StatsRow, PricingCards, FeatureRows, the whole collection as live cards, CompareTable, FaqSection, CtaSection), each labelled "built with" its readout and measured cost. Carousel: the previous/next buttons are positioned with plain insets (anchoring a scroll-button pseudo-element to its own container landed both on one spot). /docs opens on Installation. Home: the preset demo cards are sage and numbered, the navigation counter prints its numbers in the measured colour, developer-tool rows light up amber on hover; every Pro stage reads its counts from the catalogue instead of a typed number.
    • Site upgrades: the measured colour is sage instead of cyan (an autumn pair with amber, and the one hue the hero arc never crosses); every section heading and grid reveals on scroll; a tile's stage morphs into the component page's demo through transition:name; the hero field sinks and dims on a view() timeline as it scrolls out; hovering the showcase ticker slows everything in it to a quarter speed instead of stopping it; the tune panel prints the prop line and keeps its choice in the URL, as does every Playground; a readout is a button that copies its line; and a motion switch in the header previews prefers-reduced-motion.
    • Integration: motionAttribute option mirrors every reduced-motion rule to html[data-ma-motion="reduce"] at build; prefersReducedMotion() reads the attribute.
    • Home section 03 rebuilt as six effect tiles: one effect a cell, its name in the corner, nothing overlapping; an off switch leaves the tile with its name.
    • Base tokens: --ma-dark, a 0/1 scalar that follows the theme so a component can lean lighter or darker in calc() without repeating the three-state rule.
    • Browser support: abs()/mod() and container query units added to the audit; GlyphField is labelled newly with its fallback stated.
  19. 0.2.0 (unreleased): the redesign

    • 36 primitives: Accordion, Aurora Background, Border Beam, Card Stack, Carousel, Count Up, Cursor Grid, Dialog, Dot Pattern, Drawer, Gradient Text, Grid Pattern, Horizontal Scroll, Magnetic, Marquee, Noise, Page Transition, Parallax, Popover, Progress, Ripple, Rotating Text, Scroll Progress, Scroll Reveal, Scroll Scrub, Shiny Text, Skeleton, Spinner, Split Text, Spotlight, Tabs, Text Scramble, Tilt, Toast, Typewriter, Weight Wave.
    • 26 Pro sections composed from the primitives: 404, Blog Grid, Call to Action, Changelog, Comparison Table, Docs Shell, Editorial Hero, FAQ, Feature Atlas, Feature Rows, Field Hero, Footer, Hero Section, Integration Grid, Logo Wall, Mega Menu, Newsletter, Pricing Cards, Pricing Table, Stats Row, Steps / Timeline, Sticky Nav Bar, Team Grid, Terminal Hero, Testimonial Cards, Testimonial Marquee. Demos public, source gated.
    • Motion theme: five sampled springs (snap · ui · gentle · lively · ambient) as linear() curves with settle-time durations; one data-ma-preset attribute retunes every component. No component hard-codes a curve any more.
    • Browser support stated per component from @mdn/browser-compat-data, printed on every card, with the fallback named for the scroll-driven family and the carousel chrome.
    • Marquee: min-width: 0, so a strip inside a grid column no longer locks the column to its content width.
    • Aurora rebuilt: three coprime drift periods, an accelerated hue rotation, film grain stepped at 12 fps.
    • Site: a playground on every component page (knobs from the running instance, replay), ⌘K search, Open Graph cards for every page, /compare with the same hero as a React island measured side by side (22 kB raw against 374 kB raw), and a mobile menu.
    • Site rebuilt showcase-first: a cursor field hero, a numbered atlas of live tiles with readouts and costs, a preset switcher with zero JavaScript, a ticker of ten live components, and a catalogue you can filter by cost, category, trigger, support and the library you are leaving.
  20. 0.1.0

    • Catalogue: Reveal, Split Text, Typewriter, Rotating Text, Marquee, Aurora, every one with a measured byte cost, a reduced-motion branch and a page showing the source.
    • Runtime: onMount / onPage / prefersReducedMotion / refresh. It binds now and after every ClientRouter navigation, never double-binds.
    • Adapters: gsapPage (context + revert on the Astro lifecycle, ScrollTrigger refresh after paint and fonts) and lenisPage (one instance per document, resynced after each swap).
    • Verified in a real browser on Astro 5.18, 6.4 and 7.3, static and server output, with real gsap and lenis installs.
    • Site: per-component pages, categories, docs, guides, setup, MCP page, llms.txt, Markdown variant of every page, shadcn registry at /r/.
    • MCP server @moonarc/mcp: search_motion, get_component, get_install_command, get_usage.
  21. 0.0.1

    • Name-claiming placeholders published on npm, under the library's earlier name.