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

Using NucleusKit Elements

Every NucleusKit element obeys the same contract: it is an Adapter, bridging one protocol and the document. Learn that contract once and any package page becomes readable in a minute.

The contract

Attributes in. Options are attributes. Booleans are presence attributes. Names always contain a dash, so they never collide with native attributes.

<include-content lazy-load template-ref="/views/profile.html"></include-content>

Attributes out. Primitive lifecycle and status appear as state attributes: is-loading, is-success, is-error, is-active, has-rendered, did-fail. Those are the attributes CSS and Quark select on.

provider-fetch[is-loading] { opacity: 0.5; }

Events out. Custom events carry the tag name as a prefix (super-form-success, spa-route-render) so they never shadow native ones. Most bubble, which means a single listener higher in the tree can hear an entire subtree.

Data out. Providers — and a few others — publish a rich payload as a provision: the parsed response, route params, a geolocation reading. Quark reads it with prop("provision"):

provider-fetch[is-success] { $user: prop("provision").body; }

Default actions. Some events are cancelable. Call preventDefault() from a handler and the element skips whatever it would have done next. That is the escape hatch for the uncommon "almost, but not quite" case.

Recognized elements. Elements never invent opinionated children; they may recognize the children you write. super-form expects your <form>. content-tabs expects your content-tabs-header and content-tabs-body. Package pages list exactly what each element looks for.

Reading a package page

Section What it tells you
Subtitle + demo The one job, shown in the fewest possible lines
Features The use cases it covers, in plain terms
Attributes option (you set it) vs state (the element sets it), types, defaults, allowed values
Fires / Listens for Every event, when it fires, and the shape of event.detail
Default actions What preventDefault() will skip
Recognized elements The descendants it wires up automatically
CSS Classes, variables, and aliases you can hook into or opt-out of

Naming tells you the shape

NucleusKit elements follow the recommended naming conventions.

Prefix Meaning Examples
super-* Wraps and upgrades a native element you still write yourself super-form, super-input
content-* Expects children and manages their presentation content-tabs, content-drawer, content-carousel
provider-* Publishes data for the page to consume provider-fetch, provider-geolocation, provider-orientation
spa-* Routing and navigation spa-manager, spa-route, spa-a
detect-* Reflects the environment as selectable attributes detect-browser, detect-features, detect-media

Families. Some jobs need a small group of tags that only make sense together (content-tabs + content-tabs-header + content-tabs-body, spa-manager + spa-route). The parent coordinates its own family — that is expected, and it is the one place an element manages something other than itself. Families share a name prefix so the relationship is visible in markup.

The catalog

Layout — content-carousel, content-drawer, content-tabs, dialog-anchor, dismiss-watcher, data-table

Content / routing — include-content, spa-route (with spa-manager, spa-a), scroll-into-view

Forms / auth — super-form, super-input, web-authn

Providers — provider-fetch, provider-geolocation, provider-orientation, provider-storage

Platform — detect-browser, detect-features, detect-media, dom-observer, event-handler, network-status, service-worker

Orchestration — quark-sheet

The sidebar lists every package with its current API. nucleus-kit installs the whole catalog at once; each package also installs alone — if you end up using only a few elements, install those packages à la carte and skip the bundle.

Three you'll use constantly

include-content renders a view when and where you need it — lazily on scroll, on idle, on an event, or immediately — from a <template> or a URL. It is the unit of composition for views. See Building Views.

provider-fetch turns a URL into a provision. Set api-url, read prop("provision") in Quark, react to is-loading / is-success / is-error. Pair it with is-paused to hold a request until the page is ready.

event-handler stitches behavior together: turn any event into a custom event or a command aimed at any element (target-ref), listen globally for keyboard shortcuts, or debounce input. Most "glue" that would otherwise be a click handler becomes one of these tags — or, where a sheet is already present, an @on block with @dispatch / @command (see Orchestrating). Often no tag is needed at all: elements accept their imperatives as native commands, so a plain <button command="--toggle" commandfor="menu"> drives a drawer with nothing in between.

<button type="button" command="--toggle" commandfor="menu">Menu</button>
<content-drawer id="menu" class="absolute">
  <nav>…</nav>
</content-drawer>
<event-handler class="tag-backdrop" role="presentation" target-ref="content-drawer:has(+ :scope)" command-name="--close"></event-handler>

Every command an element accepts (--fetch, --submit, --reload, --open) is listed on its package page under Commands. A <button> reaches its target by id; <event-handler command-name> reaches it by selector and can relay any event (listen-for="super-form-success").

Native elements are Adapters too

<details>, <dialog>, <form>, <input>, and <select> already bridge a protocol — disclosure, the top layer, submission, the keyboard — carrying its state as attributes and firing events. Prefer their native state before inventing your own:

details[open] #status { content: "Open"; }
details:not([open]) #status { content: "Closed"; }

The NucleusKit catalog exists to give the same contract to protocols the platform does not cover yet — not to replace what it already does.

Two habits worth forming

  • Select on state, not on classes. Setting attributes is recommended over toggling / mutating classes and ids, since the latter has a heavier impact on Quark's performance. Keep classes for static styling.
  • Prefer attributes before upgrade. If script runs before an element's definition has loaded, setAttribute() is in the document at once, for CSS and Quark to select on; an assignment to a declared property is applied when the element upgrades.

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.