Skip to content

Setup · about five minutes

Set up Moonarc in five minutes.

One command installs the packages and registers the integration. Then you import a component, build, and check what it costs. The steps also cover the settings that differ per project, your coding agent, and the fixes for common problems.

  1. 01Installone command
  2. 02Add a component3 examples
  3. 03Match your project4 settings
  4. 04Connect your agentMCP and registry
  5. 05Check the build3 checks
  6. 06Troubleshooting8 fixes
npx astro add moonarc✔ Resolved packages.  Astro will run the following command:  npm i moonarc @moonarc/core✔ Dependencies installed.  Astro will make the following changes to your config file:+ import moonarc from 'moonarc';+ integrations: [moonarc()],success Added the following integration to your project:  - moonarc
The astro add log, shortened. It asks before installing and before editing your config; --yes answers both.
Works with
  • Astro 5, 6, 7
  • static and server
  • with or without ClientRouter
  • with or without Tailwind
Requirements →
01Install

Install with one command.

astro add installs Moonarc's two packages with your package manager, then adds the integration to your Astro config.

Integration options →
npx astro add moonarc
pnpm astro add moonarc
yarn astro add moonarc
bunx astro add moonarc

What changes in your project

astro.config.mjs
  import { defineConfig } from 'astro/config';
+ import moonarc from 'moonarc';

  export default defineConfig({
+   integrations: [moonarc()],
  });

In package.json, moonarc and @moonarc/core join dependencies. Nothing else in your project is touched.

What the integration adds to every page

  1. 1

    The tokens: base.css

    The easing, duration and colour custom properties every component reads, in @layer theme and inside :where(), so any rule of yours overrides them.

  2. 2

    The gate: an inline script of 92 B raw

    It sets data-ma-js on <html> before first paint and again after every ClientRouter swap. Entrance effects hide content only behind it, so a page whose JavaScript never runs still shows everything.

  3. 3

    No components and no runtime

    JavaScript arrives only with a component you import, and each component's page prints what it costs.

Install without the integration

Install the components package, then do by hand what the integration does: import the tokens once and add the gate to your layout's <head>.

terminal
npm install @moonarc/core
src/layouts/Layout.astro
---
// src/layouts/Layout.astro
import '@moonarc/core/styles/base.css';
import { gateScript } from '@moonarc/core';
---
<html lang="en">
  <head>
    <style is:inline>@layer theme, base, components, utilities;</style>
    <script is:inline set:html={gateScript} />
  </head>
  <body><slot /></body>
</html>
02First component

Import a component and use it.

Each component has its own import path, so one import never pulls another component's CSS or script. The code on the left renders what is on the right.

All 102 components →
Example
src/pages/index.astro
---
import Reveal from '@moonarc/core/Reveal';
import RotatingText from '@moonarc/core/RotatingText';
---
<Reveal>
  <h1>
    Build
    <RotatingText words={['faster', 'lighter', 'calmer']} fluid />
    Astro sites.
  </h1>
</Reveal>

Resultlive

Build faster, lighter, calmer Astro sites.

<Reveal> + <RotatingText> · 3.0 kB raw JS on the page

Reveal is 1.3 kB raw plus the shared runtime, once per site. RotatingText is CSS: 0 B.

src/components/Stats.astro
---
import CountUp from '@moonarc/core/CountUp';
---
<p><CountUp to={102} /> primitives</p>
<p><CountUp to={67} /> of them at 0 B of JavaScript</p>

Resultlive

102primitives

67of them at 0 B of JavaScript

<CountUp> · 3.0 kB raw JS on the page

CountUp counts in CSS and has no script of its own. It starts when Reveal sees it, so the page ships Reveal and the runtime.

src/components/WorksWith.astro
---
import Marquee from '@moonarc/core/Marquee';
---
<Marquee label="Works with" duration={24}>
  <span>Astro 5</span> <span>Astro 6</span> <span>Astro 7</span>
  <span>static</span> <span>server</span> <span>ClientRouter</span>
  <span>Tailwind v4</span> <span>plain CSS</span>
</Marquee>

Resultlive

Astro 5Astro 6Astro 7staticserverClientRouterTailwind v4plain CSS

<Marquee> · 0 B JS on the page

Two copies of the row and one CSS animation. It pauses on hover and stops under reduced motion.

03Your project

Four settings that differ per project.

Pick what describes your project. Every combination works without a required change; the panel shows the optional ones.

Styling

How do you write CSS?

Navigation

Do you use Astro's client router?

Output

What does astro build produce?

Colours

Where do your colours come from?

For your project

Required changes: none

  1. 01

    Styling: Plain CSSno change

    Components ship their own CSS in cascade layers and read the tokens from base.css. Any rule of yours wins without !important.

    Styling: Tailwind v4optional

    Import the token layer after Tailwind, and the library's curves and durations become utilities: ease-out-quint, duration-fast, ease-snap.

    src/styles/global.css
    @import 'tailwindcss';
    @import '@moonarc/core/styles';
    Motion tokens →

    Styling: Tailwind v3no change

    Skip the token layer: it is written for v4's @theme. Read the tokens as custom properties instead.

    any stylesheet
    .card {
      transition: translate var(--ma-duration) var(--ma-ease-out);
    }
  2. 02

    Navigation: Full page loadsno change

    Every component starts on page load and needs nothing else.

    Navigation: <ClientRouter />no change

    Components rebind after every navigation and clean up before each swap. For a script of your own, use the runtime's onMount instead of an astro:page-load listener.

    your script
    import { onMount } from '@moonarc/core/runtime';
    
    onMount('[data-menu]', (menu, { signal }) => {
      menu.addEventListener('click', () => menu.toggleAttribute('data-open'), { signal });
    });
    GSAP and Lenis adapters →
  3. 03

    Output: staticno change

    Checked in CI on Astro 5, 6 and 7 by building a real project and opening it in a browser.

    Output: serverno change

    Checked the same way with the Node adapter. vite.ssr.noExternal is not needed. If your setup externalises .astro files anyway, turn on the escape hatch.

    astro.config.mjs
    integrations: [moonarc({ noExternal: true })],
    The integration →
  4. 04

    Colours: Defaultsno change

    The primitives read --ma-* tokens with light and dark values. Override any of them in your own :root.

    Colours: A shadcn themeno change

    Pro sections read shadcn's variables (--background, --primary, --card, --radius) as they are, so a theme from shadcn init or tweakcn applies without mapping.

    Theming →

    Colours: One brand colouroptional

    theme.css derives a brand scale, a tinted neutral and an accent from one colour, in CSS at runtime.

    src/styles/global.css
    @import '@moonarc/core/theme.css';
    
    :root {
      --ma-brand: oklch(0.543 0.097 235);
    }
    Theme builder →
04Agents

Connect your coding agent.

The MCP server runs on your machine over stdio and needs no account. Your agent can search the catalogue by intent and JavaScript budget, read how a component is built, and get the exact install command.

MCP server →
# in your project
claude mcp add moonarc -- npx -y @moonarc/mcp
// .cursor/mcp.json
{
  "mcpServers": {
    "moonarc": { "command": "npx", "args": ["-y", "@moonarc/mcp"] }
  }
}
// .vscode/mcp.json
{
  "servers": {
    "moonarc": { "type": "stdio", "command": "npx", "args": ["-y", "@moonarc/mcp"] }
  }
}
# ~/.codex/config.toml
[mcp_servers.moonarc]
command = "npx"
args = ["-y", "@moonarc/mcp"]

A prompt to start with

prompt
Add a strip of our customer logos under the hero. Use Moonarc through its MCP server, and only components that ship 0 B of JavaScript.

The agent calls search_motion with max_js_bytes: 0, gets Marquee and the other components with no script of their own, then asks for the install command.

What your agent can ask

search_motion(intent, max_js_bytes?, tier?, limit?)
Components for an intent under a JavaScript budget in raw bytes. max_js_bytes: 0 returns only the ones with no script of their own. limit: how many, best first (10 unless set, at most 50).
get_component(name)
Source, props and craft notes: easing, duration, the reduced-motion branch, ClientRouter behaviour.
get_install_command(names, method?)
The exact command. The agent runs it; the server never writes files.
get_usage()
Plan and quota. The free tier needs no account and has no limit.

Copy the source with the shadcn CLI

The CLI reads components.json, and an Astro project has none. shadcn init would create one and also add React and seven packages these components never import. Write the file yourself instead: package.json stays as it is.

components.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/styles/global.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "lib": "@/lib"
  },
  "registries": {
    "@moonarc": "https://moonarc.dev/r/{name}.json"
  }
}
tsconfig.json
{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist"],
  "compilerOptions": {
    "paths": { "@/*": ["./src/*"] }
  }
}
terminal
npx shadcn@latest add @moonarc/reveal

Files land in src/components/moonarc/ and src/lib/moonarc/, Pro files in a pro/ folder inside each, and the stylesheets in src/styles/. They import one another through the @/* alias, so tsconfig.json needs the paths entry above. tailwind.css names the stylesheet the theme item writes its variables into.

Without MCP: /llms.txt is the whole catalogue in one file, and every component and docs page has a Markdown twin at its URL plus .md.

05Check

Check that it worked.

Build the headline example from step 02 and look at what Astro wrote. Three things show that the setup is right.

terminal
npx astro build
npx astro preview
dist/index.html, shortened
<head>
  <link rel="stylesheet" href="/_astro/index.[hash].css">
  <script>(f=>addEventListener('astro:after-swap',f,f()))(()=>document.documentElement.dataset.maJs=1)</script>
</head>
<body>
  <div data-ma-reveal="true" class="ma-reveal">
    <h1>Build <span class="ma-rotate">…</span> Astro sites.</h1>
  </div>
  <script type="module">/* Reveal and the runtime */</script>
</body>
  1. The gate is in <head>

    One inline script of 92 B raw that sets data-ma-js. If it is missing, reveals show at once and never animate in.

  2. The only JavaScript is Reveal and the runtime

    3.0 kB raw as measured in CI, gate included. RotatingText adds nothing. Astro inlines a script under 4 kB raw into the page (Vite's build.assetsInlineLimit), so a small site can have no .js file in dist/_astro at all.

  3. It moves, and respects reduced motion

    In the preview, the heading rises in and the word cycles. Turn on reduce motion in your operating system and reload: the heading fades in place and the words crossfade.

Using the client router? Click to another page and back: the heading animates again. How that works

06Fixes

If something is off.

Each entry is a symptom, its cause and the fix.

Cannot resolve "@moonarc/core/…"

The components live in @moonarc/core. If only moonarc is in your package.json, pnpm and Yarn Plug'n'Play will not let your code import the other package. astro add installs both; after a manual install, add it:

pnpm add @moonarc/core

Reveals are visible at once and never animate in

The html[data-ma-js] gate is missing. The integration adds it. After a manual install, put <script is:inline set:html={gateScript} /> in your <head>, with gateScript imported from @moonarc/core. Without it every page still shows all its content; nothing hides, so nothing animates in.

Things fade but never move

Reduced motion is on. Every component follows prefers-reduced-motion: reduce: Reveal keeps a short fade and drops the travel, RotatingText crossfades in place, Marquee stops. Check your operating system's reduce motion setting. On this site, the motion switch in the header does the same.

Accessibility

Animations stop after clicking a link

A script outside the library listens to astro:page-load once, or a GSAP or Lenis setup is not wrapped. Bind your own code with the runtime and wrap GSAP and Lenis with the adapters.

Styles are missing inside a partial

Astro drops <head> content, styles included, from pages with export const partial = true. The page that receives the partial has to import the component, or its stylesheet, itself.

Tailwind v3: ease-out-quint does nothing

The token layer uses v4's @theme, which v3 does not read, so the utilities never exist. On v3, use the custom properties directly: transition-timing-function: var(--ma-ease-out).

Colours ignore my dark mode toggle

The tokens follow the system setting unless <html> says otherwise: data-theme="dark" or class="dark" forces dark, data-theme="light" or class="light" forces light. If your toggle writes a different attribute, set one of these as well. The library's ThemeToggle writes data-theme.

ERR_UNKNOWN_FILE_EXTENSION ".astro"

Not expected on Astro 5, 6 or 7: CI builds a server project on each without it. Pass moonarc({ noExternal: true }) and open an issue with your config.

07Next

Where to go from here.