@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; }

Building Views

The Nucleus Stack has no components. When you need a reusable piece of UI with its own behavior, you write a view: an HTML fragment that owns its scoped CSS and Quark sheets. A JS Module is optional. Views are composed by the elements that load them.

Anatomy of a view

views/
  profile/
    profile.html    # markup: one root element, bind-* targets, <template>s
    profile.css     # styles scoped to the root
    profile.quark   # orchestration scoped to the root
    profile.js      # custom logic functions, called from Quark.
<section id="profile">
  <link rel="stylesheet" href="/views/profile/profile.css">
  <quark-sheet src-url="/views/profile/profile.quark"></quark-sheet>
​
  <provider-fetch api-url="/api/me">
    <h2 bind-name></h2>
    <ul bind-roles>
      <template><li></li></template>
    </ul>
  </provider-fetch>
</section>
@use "/views/profile/profile.js" as profile-module;
​
/* profile.quark — host is #profile */
provider-fetch[is-success] {
  $me: prop("provision").body;
  [bind-name] { content: profile-module.formatName($me.name); }
  [bind-roles] { content: iterate($me.roles); }
  [bind-roles] li { content: item; }
}

Two rules keep views portable:

  1. One root element. That root is the sheet's host, so rules cannot leak and the view can be dropped anywhere.
  2. Re-derive your own context, if necessary. Like CSS variables, Quark variables also cascade. If the parent view defines the desired variable, use it. If not, define it on the ancestor. You will need to use <quark-sheet is-global>.

Loading views

A <script> inside a view fetched from a URL does not run; its <link rel="stylesheet">, <style> and <quark-sheet> do. A <template> parsed with the page does run its scripts once rendered. Load element definitions from the page.

include-content

The workhorse. It renders a <template> or a remote fragment when and where you want it.

<include-content lazy-load template-ref="/views/comments/comments.html"></include-content>
<include-content is-active template-ref="#empty-state"></include-content>
<include-content idle-load pre-fetch="idle" template-ref="/views/settings/settings.html"></include-content>
<include-content is-active><template>Recognized by default.</template></include-content>
  • Lazy (un)load — render once scrolled into view, and optionally unrender once it leaves. observer-root-margin lets you start early.
  • Eager / idle — render immediately (is-active), or when the browser is idle (idle-load).
  • Conditional — toggle is-active from Quark to show or hide.
  • Prefetch — pre-fetch="idle" warms a remote template so activation is instant.
  • Keep state — persist-content reuses the same tree across toggles instead of rebuilding.

Because is-active is just an attribute, conditional rendering is a Quark rule:

main[data-mode="edit"] include-content[bind-editor] { is-active: ""; }
main:not([data-mode="edit"]) include-content[bind-editor] { is-active: none; }

spa-route

Routing is a set of tags. Each spa-route matches a URL pattern and renders a view; spa-manager (which is optional) batches changes into view transitions; spa-a navigates.

<spa-manager>
  <spa-route route-href="/" template-ref="/views/home/home.html"></spa-route>
  <spa-route route-href="/users/:id" template-ref="/views/user/user.html"></spa-route>
  <spa-route route-regex=".*" template-ref="/views/not-found/not-found.html" is-fallback></spa-route>
</spa-manager>
​
<nav>
  <spa-a route-href="/">Home</spa-a>
  <spa-a route-href="/users/42">Me</spa-a>
</nav>

Route params are a provision.

spa-route[is-active] {
  $params: prop("provision").params;
​
  &[template-ref$="user.html"] provider-fetch {
    api-url: "/api/users/#{$params.id}";
  }
}

Nested layouts (match-nested), scroll restoration, history actions, and per-direction transition styling are all attributes on these tags. See the spa-route package for the full set.

An app skeleton

<body>
  <quark-sheet src-url="/shell.quark"></quark-sheet>
​
  <include-content is-active template-ref="/views/header/header.html"></include-content>
​
  <spa-manager>
    <spa-route route-href="/" template-ref="/views/home/home.html"></spa-route>
    <spa-route route-href="/settings" template-ref="/views/settings/settings.html"></spa-route>
  </spa-manager>
</body>
/index.html
/shell.quark          # site-wide rules (theme, auth state, global shortcuts)
/shell.js             # optional pure functions any sheet may @use. Simple applications may prefer to keep all functions in this single file.
/shell.css            # shared layout
/views/<name>/<name>.{html,css,quark}

No build process is required. Files are served as-is, views load on demand, and editing any file is a refresh away.

Data between views

Nested view communication and shared data is done the same way everything else is:

  • Down through the document: a parent sets an attribute or publishes a provision; a descendant view reads it.
  • Up through events: a view's element fires (or a sheet's @on block @dispatches); an ancestor's sheet or event-handler listens. Events bubble, so a single rule at the root can hear the whole app.
  • Across through shared state / Quark variable on a common ancestor.

When two views need the same value, put it on their nearest common ancestor and let both read it. Do not reach for JavaScript to pass it around.

Name every custom attribute with a dash (data-duration, is-open), never a bare word (duration): a bare name can shadow, or later collide with, a native attribute. Form field names follow the same rule when an @on input block copies them onto the host (data-trip: event.target.form.elements["data-trip"].value).

Long lists

iterate() keeps the DOM proportional to your data. For heavy rows, stamp a lightweight <include-content lazy-load> per item and put the expensive markup in a commonly referenced template; only rows on screen materialize, and they can unrender as they scroll away (via the lazy-unload attr). This will ensure linear performance: only a single node per iteration. That covers most lists comfortably. At six figures of rows, node count itself becomes the limit — see Limitations.

Enhancing existing static pages

Everything above works on a server-rendered or CMS page. Start with one element and one sheet inside one section. The rest of the page is unaffected, and there is no rewrite waiting at the end.

Load order

Put <quark-sheet> first inside its host so it registers before sibling elements connect. Prefer reacting to state attributes (is-success, is-active) over one-shot events for anything that can happen during boot. See Orchestrating.

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.