@use "/shell" as *; @use "quark:list" as list; #release-notice { /* opens once: sheet re-runs restart a delay, so gate it on the fact it writes */ &:not([data-did-open]) { @delay 7000 { is-open: ""; data-did-open: ""; } } /* prevent clicks from bubbling to the sheet */ @on mouseup, click (stop-propagation); } provider-fetch[api-url*="package-metas/index.json"][is-success] { $package-indices: prop("provision").body; /* `excom.navGroup` moves a package out of its type's list, into that group */ $ungrouped: list.reject($package-indices.packages, "navGroup"); .package-links { [bind-elements] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "kit-element")); } [bind-element-bases] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "element-base")); } [bind-tools] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "tool")); } /* the standalone libraries: same rows, rendered flat (shell.css) */ [bind-libraries] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "library")); } [bind-library-group] ul:not(ul ul) { content: iterate(list.filter($package-indices.packages, "navGroup", "libraries")); } ul:not(ul ul) > li { $pkg: item.shortName; > spa-a { route-href: "/packages/#{item.shortName}"; } } /* one `content` rule per link: a second one would rewrite the first's text on every pass */ ul:not(ul ul, [bind-libraries] ul) > li > spa-a { content: item.shortName; } [bind-libraries] ul:not(ul ul) > li > spa-a { content: displayName(item.shortName); } /* packages whose docs span several pages: one collapsible group per section */ [bind-sections] { content: iterate(item.docSections); summary { content: item.title; } details > ul { content: iterate(item.docs); spa-a { route-href: "/packages/#{$pkg}/#{item.name}"; content: item.title; } } details:not(:has(spa-a[is-active])) { open: none; } } } /* page titles: the page, then the site. The docs home keeps the title in the document head; the other routes carry theirs in markup */ spa-route[is-active] { /* a site guide */ &[route-href$=":name"] { $title-guide: list.find($package-indices.docs, "name", $route.params.name); document-title: "#{$title-guide.title or $route.params.name} · Nucleus · docs"; } /* a package README */ &[route-href$=":packageName"] { document-title: "#{displayName($route.params.packageName)} · Nucleus · docs"; } /* one page of a package's docs */ &[route-href$=":docName"] { $title-package: list.find($package-indices.packages, "shortName", $route.params.packageName); document-title: "#{docTitle($title-package, $route.params.docName)} · #{displayName($route.params.packageName)} · Nucleus · docs"; } } /* the page's markdown file, for the footer link: the docs home and each guide the index lists, and a package's page whose index entry says `markdown` (build-docs-index sets it; the dev server's index has none, so no link there). siteDocHref and the path check say which URL is the page's own: the 404 and a package's doc pages have none */ > spa-manager[active-url] { $page-path: attr("active-url").split("#").at(0).split("?").at(0); $page-name: if($page-path == SITE_HOME: SITE_HOME_DOC; else: $page-path.split("/").at(-1)); $page-guide: list.find($package-indices.docs, "name", $page-name); $page-package: list.find($package-indices.packages, "shortName", $page-name); $page-markdown: if($page-guide and siteDocHref($page-guide.name) == $page-path: "/docs/#{$page-guide.name}.md"; $page-package.markdown and "#{SITE_BASE}/packages/#{$page-package.shortName}" == $page-path: "/#{$page-package.shortName}.md"); footer [bind-page-markdown] { href: $page-markdown; content: ternary($page-markdown, "Markdown version of this page"); } } } spa-route { $route: prop("provision"); /* paramless routes name their guide in markup (the docs home, `/`); /docs/:name gets it from params */ $route-doc-name: attr("data-doc-name"); } /* the desktop aside and the mobile sheet stamp the same nav template */ [data-site-nav] { details:has(spa-a[is-active]) { /* not kosher */ open: ""; } spa-a[is-active] { aria-current: ""; } spa-a:not([is-active]) { aria-current: none; } } /* mobile sheet: Escape / back gesture while open; a tapped link closes it */ #site-menu { &[is-open] dismiss-watcher { is-active: ""; } &:not([is-open]) dismiss-watcher { is-active: none; } @on click (target: "spa-a") { is-open: none; } } #site-menu-gesture { &:has(> #site-menu[is-open]) { progress-offset: 1; } &:not(:has(> #site-menu[is-open])) { progress-offset: 0; } @on gesture-handler-start { #site-menu { is-scrubbing: ""; } } @on gesture-handler-end { #site-menu { is-open: event.detail.snap == 1; is-scrubbing: none; } } } main { @on copy-source (handle: copySource); } #search-dialog[open] { > include-content { is-active: ""; /* for some reason this is necessary on first render */ @on include-content-did-render (handle: focusInput); } input[type="search"] { /* works on all subsequent opens */ autofocus: ""; } } #search-dialog:not([open]) input[type="search"] { autofocus: none; }

Quick Start

One HTML file is enough. No install, no build, no framework.

What follows is our Todo App example, verbatim, with one deliberate change — it reads from the public JSONPlaceholder API and sorts the list with a built-in module. Open the example to edit any of it live.

1. Load NucleusKit

nucleus-kit packages the elements, Quark, and Valence.css behind a single import.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My first Nucleus app</title>
    <link rel="stylesheet" href="https://unpkg.com/@excom/nucleus-kit/dist/basic.min.css">
    <script src="https://unpkg.com/@excom/nucleus-kit/dist/index.umd.min.js"></script>
  </head>
  <body>
    <main></main>
  </body>
</html>

Want a package manager instead? npm install @excom/nucleus-kit, then import "@excom/nucleus-kit" and @import "@excom/nucleus-kit/basic.css". Every element is also published independently — any package page covers the slim install.

2. The markup

Drop this <article> into the <main>, and save the two files beside it. It is the whole app: a provider that fetches, a <template> describing one row, and a form per action. No ids, no classes, nothing dynamic — every fact arrives later, as an attribute.

<article>
  <link rel="stylesheet" href="/todo-app.css">
  <quark-sheet src-url="/todo-app.quark"></quark-sheet>
  <provider-fetch api-url="https://jsonplaceholder.typicode.com/todos?_limit=5">
    <ul>
      <template>
        <li>
          <super-form>
            <form method="patch">
              <input type="checkbox" name="completed">
              <input name="title" required autocomplete="off">
            </form>
          </super-form>
          <super-form>
            <form method="delete">
              <button type="submit"></button>
            </form>
          </super-form>
        </li>
      </template>
    </ul>
    <super-form>
      <form action="https://jsonplaceholder.typicode.com/todos" method="post">
        <input name="title" required autocomplete="off" placeholder="New Reminder">
        <button type="submit"></button>
      </form>
    </super-form>
  </provider-fetch>
</article>

provider-fetch does the reading and publishes the response for the sheet to pick up. super-form wraps a <form> you write yourself and submits it over fetch, so method="patch" and method="delete" work where the browser only offers GET and POST.

JSONPlaceholder returns { userId, id, title, completed } — the same field names the example already binds, so title and completed are untouched. Only the URLs changed: /api/todos became the JSONPlaceholder URL, and ?_limit=5 keeps the list to five rows.

3. The rules

A <quark-sheet> observes its parent and applies rules to everything inside it. Read it like CSS: when the provider succeeds, take its body, stamp one <li> per todo, and fill each row from the item.

@use "quark:list" as list;
​
provider-fetch[is-success] {
  $todos: prop("provision").body;
  ul {
    content: iterate(list.sort-by($todos, "title"), none, "id");
    form {
      action: "https://jsonplaceholder.typicode.com/todos/#{item.id}";
    }
  }
  input[name="completed"] {
    checked: item.completed;
  }
  input[name="title"] {
    value: item.title or "";
  }
  super-form:has([name="completed"]) {
    /* Submits form when inputs change. Uses native Command. */
    @on change { @command --submit; }
  }
  /* Re-fetches todos after successful form submission. */
  @on super-form-success { @command --fetch; }
}

Nine declarations carry the entire app. iterate() is keyed on "id", so a re-read reuses the rows it already has. Every row's action is written from its own item, which is why one <template> serves every todo. The two @on blocks close the loop: a changed field submits its form, and any form that succeeds tells the provider to read the list again.

Calling a module

The one deviation from the example app is list.sort-by($todos, "title"). @use "quark:list" as list; imports one of Quark's built-in modules, with no file and no fetch, and namespaces its functions under list; sort-by returns a sorted copy for Quark to render.

Your own module loads the same way (@use "./utils.js" as utils;) and should hold the same kind of function: one that receives values as arguments, returns a value for Quark to write, and has no idea a document exists. Querying the DOM and writing to it is the Orchestrator's job, and Quark is already doing it. Reach for a module of your own only when the built-in modules fall short. Business Logic covers where that line sits and why calculations belong on this side of it.

4. The styling

Trimmed to what you need to see it work — the example carries the full file. The first two rules are the interesting ones: CSS selects on the same facts Quark writes, so an error message and a struck-through title need no extra state.

@scope {
  /* data-driven css */
  provider-fetch[is-error] ul::before {
    display: block;
    padding: 0.6rem 1rem;
    color: var(--v-del-color);
    font-size: 0.875rem;
    content: "Could not load todos.";
  }
  li:has(:checked) input[name="title"] {
    color: var(--v-muted-color);
    text-decoration: line-through;
  }
​
  /* add cosmetic styling here */
}

5. What you will see

JSONPlaceholder fakes every write. A POST, PATCH or DELETE answers as if it worked — POST returns the new todo with id: 201, PATCH echoes the merged object, DELETE returns {} — but nothing is stored. Since this app re-reads the list after every successful write, the server's unchanged answer wins:

  • Check-off a todo and it PATCHes, re-reads, and snaps back to whatever JSONPlaceholder still says.
  • Add a todo and it POSTs, re-reads, and the new row disappears.
  • Delete a todo and it DELETEs, re-reads, and the row returns.

That is the API being honest about being a fixture, not the app being broken. Point api-url and the two action URLs at a real endpoint and every one of those actions sticks, with no other change.

What to try

  • Tick a todo, then watch the network panel: one PATCH, one GET, and the row restored.
  • Add "Write tests" and watch it vanish on the re-read.
  • Reverse the order with list.sort-by($todos, "title", "desc").
  • Drop ?_limit=5 to render all 200 rows, and note that nothing else has to change.

What just happened

  • Elements owned their own behavior and reported state through attributes such as is-success.
  • The document held every fact the app knows.
  • Quark observed that state and cascadingly updated it in response.

That loop is the entire architecture. Core Concepts walks through it in one sitting.

Next steps

  • Quick Start - A working page, in five minutes.
  • Core Concepts - The mental model, in one sitting.
  • Using Elements - The NucleusKit catalog and how elements behave.
  • Orchestrating - Get familiar with Quark.
  • Styling - Valence.css themes, tokens, and state-driven CSS.
  • Building Views - Structure a real app: routes, views, lazy loading.
  • Other Guides - Business Logic, Creating Elements, Best Practices, Troubleshooting, Debugging with Agents
  • Diving Deeper - The architecture behind it all, for the curious and the skeptical.

Beta. The Nucleus Stack is in beta for a few weeks until features are stabilized and optimized.

Thanks — we'll email you when it ships.

Something went wrong. Please try again.