Skip to content

Components / Sections

Quickstart Steps

A quickstart as numbered steps down a rail that fills as the reader moves through them, each number lighting as its step reaches the middle of the screen. A step has its words, its code in tabs (one per language or package manager, a copy button on each) and what the reader should see when it worked, as a line or as output, so a step can be checked before the next. A last line can point to what to read next. Scroll-driven where the browser has view timelines; the rail full and every number lit where it does not. Composes TracingBeam and CodeTabs; zero JavaScript of its own.

Live demo

Quickstart

Your first email in three steps

  1. Install the SDK

    npm install sitename
    pnpm add sitename
    bun add sitename
  2. Keep your key in the environment

    Create a key in the dashboard under Keys. It is shown once.

    SITENAME_KEY=sk_test_••••

    You should see

    The SDK reads SITENAME_KEY by itself; nothing else to configure.

  3. Send your first email

    import { Sitename } from 'sitename';
    
    const mail = new Sitename();
    await mail.emails.send({
      from: 'hello@your-domain.com',
      to: 'you@example.com',
      subject: 'It works',
      text: 'Sent from the quickstart.',
    });
    curl https://api.sitename.example/v1/emails \
      -H "Authorization: Bearer $SITENAME_KEY" \
      -d to=you@example.com -d subject="It works"

    You should see

    {
      "id": "em_7Hq2kD9x",
      "status": "queued"
    }
  4. Next, hear back when it lands. Set up webhooks

Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.

Variants · each row writes the attribute the prop writes; the section's own CSS answers

tone
density

Measured

JavaScript of its own

0 B

Uses CopyButton and the shared runtime (1.9 kB raw, once per site). With those included: 2.5 kB raw.

CSS 21.6 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Limited availability

Chrome 115 · not Firefox · Safari 26; needs scroll-timeline. Elsewhere: the rail is drawn full and every number lit (Firefox).

Install Pro

The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:

terminal
npx shadcn@latest add @moonarc-pro/quickstart-steps
src/pages/index.astro
---
import QuickstartSteps from '@/components/moonarc/pro/QuickstartSteps.astro';
---
<QuickstartSteps
  id="qs"
  eyebrow="Quickstart"
  next={{ text: 'Next, send it from your app.', href: '/docs/sdks/', label: 'Pick an SDK' }}
  steps={[
    {
      title: 'Install the SDK',
      code: [
        { label: 'npm', code: 'npm install sitename' },
        { label: 'pnpm', code: 'pnpm add sitename' },
      ],
    },
    {
      title: 'Send a request',
      text: 'Your key is in the dashboard under Keys.',
      code: [{ label: 'cURL', code: 'curl https://api.example.com/v1/ping -H "Authorization: Bearer $KEY"' }],
      result: { code: '{ "ok": true }' },
    },
  ]}
>
  <span slot="headline">Send your first request in <em>two minutes</em></span>
</QuickstartSteps>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: tracing-beam, code-tabs.

Props

PropTypeDefaultDescription
steps{ title; text?; code?; result? }[]noneThe steps, in order. text is a paragraph or two (a blank line splits them), with `code` in backticks; code is CodeTabs' tabs ({ label, code, lang }); result is what the reader should see: { label?, text?, code?, lang? }, the code in JSON unless lang says otherwise.
idstring'ma-qs'Prefix of the code tabs' radio names; unique on the page.
eyebrowstringnoneA short line above the heading. Slot "eyebrow" wins.
headlinestringnoneThe heading. Slot "headline" wins and may hold an <em>.
copystringnoneA line under the heading.
resultLabelstring'You should see'The words over a step's result.
next{ text: string; href: string; label: string }noneA last line on the rail with a link: what to read next.
level2 | 32Heading level of the heading; each step's title is one below. With no heading (steps inside a docs page), each step's title takes this level.
tone'surface' | 'muted' | 'ink' | 'brand'unset (paints nothing)The section's ground; on ink and brand the rail takes the tone's emphasis colour.
density'compact' | 'cozy' | 'roomy''cozy'Block padding ×0.8 / ×1 / ×1.25 through --ma-density.

Theming and overrides

The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.

Classes

.ma-qs.ma-qs__code.ma-qs__copy.ma-qs__eyebrow.ma-qs__head.ma-qs__headline.ma-qs__inner.ma-qs__next.ma-qs__num.ma-qs__result.ma-qs__rlabel.ma-qs__rtext.ma-qs__step.ma-qs__steps.ma-qs__text.ma-qs__title.ma-qs__trace.ma-trace__line

Slots

slot="eyebrow"slot="headline"

Reduced motion

The rail is drawn full and every number lit.

With ClientRouter

CSS only; the copy buttons are CodeTabs', bound through the shared runtime.

Why it is built this way

Replaces: Quickstart pages in developer docs · Step lists with code blocks (Mintlify Steps, Starlight Steps). See the migration table.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc

A code block with tabs (for example npm, pnpm, bun) and a copy button per panel.

every browserclick

A line beside an article fills as it is read, with a dot at its head.

Chrome 115 · not Firefox · Safari 26scroll

API reference

Every request takes your key as a bearer token. Sample API, made up for this demo.

Base URLhttps://api.sitename.example/v1

Language

Emails

Send an email, then follow it until it lands.

Send an email

POST/emails

Queues one email. It is sent within seconds; follow it with GET /emails/{id} or a webhook.

Body

  • fromstringFormat: email.required

    An address on a domain you have verified.

  • tostringFormat: email.required

  • subjectstringAt most 998 characters.required

  • textstringoptional

    The plain-text body.

  • tagsarray of objectsoptional

    Labels you can filter the logs by.

    Fields 2
    • namestringoptional

    • valuestringoptional

Responses

202
Queued.
422
A field is missing or the domain is not verified.

Request

curl -X POST "https://api.sitename.example/v1/emails" \
  -H "Authorization: Bearer $SITENAME_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@sitename.example",
    "to": "ada@example.com",
    "subject": "Your receipt",
    "text": "string",
    "tags": [
      {
        "name": "kind",
        "value": "receipt"
      }
    ]
  }'
const res = await fetch('https://api.sitename.example/v1/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SITENAME_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'hello@sitename.example',
    to: 'ada@example.com',
    subject: 'Your receipt',
    text: 'string',
    tags: [
      {
        name: 'kind',
        value: 'receipt',
      },
    ],
  }),
});
const data = await res.json();
import os
import requests

res = requests.post(
    "https://api.sitename.example/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['SITENAME_KEY']}"},
    json={
        "from": "hello@sitename.example",
        "to": "ada@example.com",
        "subject": "Your receipt",
        "text": "string",
        "tags": [
            {
                "name": "kind",
                "value": "receipt",
            },
        ],
    },
)
print(res.json())
{
  "id": "em_7Hq2kD9x",
  "status": "queued",
  "created_at": "2026-10-09T08:14:22Z"
}
{
  "error": "domain_not_verified",
  "message": "Verify sitename.example before sending from it."
}

Get an email

GET/emails/{id}

Path parameters

  • idstringrequired

    The id the send returned.

Responses

200
The email and where it is.

Request

curl "https://api.sitename.example/v1/emails/em_7Hq2kD9x" \
  -H "Authorization: Bearer $SITENAME_KEY"
const res = await fetch('https://api.sitename.example/v1/emails/em_7Hq2kD9x', {
  headers: {
    Authorization: `Bearer ${process.env.SITENAME_KEY}`,
  },
});
const data = await res.json();
import os
import requests

res = requests.get(
    "https://api.sitename.example/v1/emails/em_7Hq2kD9x",
    headers={"Authorization": f"Bearer {os.environ['SITENAME_KEY']}"},
)
print(res.json())
{
  "id": "em_7Hq2kD9x",
  "status": "queued",
  "created_at": "2026-01-01T00:00:00Z"
}

Domains

The domains you send from, and whether their records check out.

List domains

GET/domains

Query parameters

  • limitinteger1 to 100.optional

    How many to return.

    Default20

Responses

200
The domains, newest first.

Request

curl "https://api.sitename.example/v1/domains" \
  -H "Authorization: Bearer $SITENAME_KEY"
const res = await fetch('https://api.sitename.example/v1/domains', {
  headers: {
    Authorization: `Bearer ${process.env.SITENAME_KEY}`,
  },
});
const data = await res.json();
import os
import requests

res = requests.get(
    "https://api.sitename.example/v1/domains",
    headers={"Authorization": f"Bearer {os.environ['SITENAME_KEY']}"},
)
print(res.json())
{
  "data": [
    {
      "name": "sitename.example",
      "status": "verified"
    }
  ],
  "has_more": false
}

An API reference in one section: a sidebar of groups and their operations; for each operation its method and path, what it does, its path, query and header parameters, the fields of its body (nested fields under a disclosure, as deep as they go) and its responses; and beside it, sticky on a wide screen, the request in every language with a copy button and the response examples by status.

newly · Chrome 131 · Firefox 143 · Safari 18.4click

Give your coding agent the Moonarc catalogue

Add the MCP server once and the agent searches components by intent and byte budget, reads their craft notes and gets the command that installs them.

  • search_motion: Components for an intent under a JavaScript budget in raw bytes.
  • get_component: Source, props and craft notes: easing, duration, the reduced-motion branch, ClientRouter behaviour.
  • get_install_command: The exact command.
# 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 feature section with code beside the copy: a headline, copy and a short list on one side, and CodeTabs on the other, one tab per client or language, each panel with its own copy button.

every browserclick

A vertical timeline: the line draws itself down the page in step with the scroll, each step revealing from the line's side as it is reached, markers numbered or dated.

Chrome 115 · not Firefox · Safari 26scroll

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown