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

Troubleshooting

Symptom first, then cause, then fix. Nearly every problem comes from one of three places: a rule that does not match what you think it does, a change Quark cannot see, or timing at startup.

A rule doesn't run

The Quark sheet did not load. A sheet that holds an at-rule Quark does not have (@media, @keyframes, @supports, …) or an !important fails to load: the console names the offender (@media is not a Quark at-rule), the sheet gets an is-error attribute, and none of its rules run. Keep those in the stylesheet. An inline sheet that fails to parse though it reads right may hold text the browser took for markup: see quark-sheet.

It's outside the host. A sheet matches only strict descendants of its parent element. Move the sheet, target the host with :scope, or use is-global if the rule truly must reach the whole document.

It targets the host without :scope. section { … } inside a sheet whose host is that section will not match it. Write :scope { … }.

It uses an unobserved selector. Interaction and validity pseudo-classes (:hover, :focus, :checked, :invalid) and pseudo-elements are not observed — the rule matches on its first run only, and the console shows a Quark warning at build. Hook element state instead: dialog:not([open]), [aria-expanded="true"]. :has(), sibling combinators and :nth-child() are observed and need no workaround.

The referenced variable doesn't exist on an ancestor. $name resolves upward from the consuming element. Confirm the declaring rule matches an ancestor (or the element itself), and that the name is spelled identically, including namespace.

A nearer binding is shadowing. When two rules write $theme at different depths, the nearest ancestor wins. Use unset on the inner one to fall through, or namespace them.

A rule ran once and never again

You read a class or id through element or closest(). Those reads are not observed. Name the class or id in the selector (li.is-done, #main) or read it with a literal attr("class"). Setting attributes is still recommended over toggling / mutating classes and ids, since the latter has a heavier impact on Quark's performance.

You changed a JS property, not an attribute. In selectors, only attributes are observed. Reflect the value with setAttribute(), or read it in an expression with a literal prop("x") — that subscribes to JS assignments of x.

The rule is reading a provision that hasn't re-published. prop("provision") re-runs when the provider assigns a new provision. Confirm the provider actually refetched (the --fetch command, a changed api-url, or is-paused removed).

Something stays set after its rule stopped matching

That's by design: rules do not revert. Write the counter-rule for every state you leave.

details[open] { --border: "red"; }
details:not([open]) { --border: "transparent"; }

Content flashes empty while loading

The expression resolved to null / undefined, which wipes the target. Keep the last good value with preserve:

#title { content: $todo.title or preserve; }

Content from a module function never appears

The target keeps what it had, and the console says Quark: content does not await a promise from a module function — return a value or a node. The function returned a promise: it is async, or it hands back the result of fetch() or another asynchronous API. Quark calls module functions synchronously on purpose.

  • Data from a server or a store belongs to an Adapter: provider-fetch publishes the response, and a rule reads it with prop("provision").
  • A library that has to load first: await it at the top level of the module. The first rule run waits for @use imports.
  • Work that depends on the arguments: create the element, return it at once, and fill it when the work finishes. See Asynchronous work.

"Loop guard: …" in the console

Two participants keep re-triggering each other and the stack cut the chain. Causes, in order of likelihood:

  • two rules that flip each other's attributes or classes (a sets x when y; b sets y when x). Give the transition one attribute with a value.
  • an element effect that writes an attribute a rule reacts to, and the rule writes the attribute the effect reacts to.
  • an attribute change fires an event (dom-observer, an element's -change event) and the listener — an @on block, an effect — changes that attribute again.
  • a rule rendering elements its own selector matches, which re-triggers the rule (see below).

Every write an engine makes carries the depth of the chain that caused it; the write that would be hop 51 (LoopGuard.limit) is dropped, logged once with the element and attribute, and published to DevTools as an orchestration error. That is all that happens: no event fires, no attribute is wiped, no sheet or element is disabled, and the app is not told beyond the console line (subscribe with LoopGuard.onTrip() if it needs to know). The document keeps the state it had before the dropped write — but the participants are wherever the cycle left them, so treat the message as a bug report, not a fix. Start from the named attribute: what writes it, what reacts to it. Then let the reaction converge (a write of the value already there is skipped), give the transition one attribute, or gate one side on a guard attribute. Rules that gate on attributes they write for each other are also warned about when the sheet builds.

A rule cannot loop on its own attribute: a declaration is never re-run by the attribute it wrote. class: is the exception — its classes are separate facts, so .is-done { class: (is-struck: true); } applies when is-done arrives — and a class cycle is cut by the guard. Effects that keep re-queuing themselves inside one element are cut the same way, after LoopGuard.limit runs in one batch. LoopGuard (@excom/kit-utils, also Neutron.DOM.LoopGuard) exposes limit, configure({ limit, log }) and onTrip().

The page freezes / "Maximum call stack"

A rule is rendering elements its own selector matches, which re-triggers the rule.

/* BAD */ span { content: template("#span-tmpl"); }

The loop guard stops this after 50 nested paints, but the fix is the selector: match the container or a bind-* attribute instead, and keep template children out of the selector's reach with a dedicated attribute or a nesting-guarded selector (section:not(section section)). A freeze the guard does not catch comes from writers it cannot see — plain setAttribute / innerHTML in app JS feeding each other — or from a loop with no DOM write in it at all.

An event fired but nothing listened

It fired before the sheet ran. Sheets loaded via src-url or with @use imports don't run until those resolve. Place the sheet first in its host, prefer inline text for boot-time listeners, or react to a state attribute (is-success) instead of the one-shot event.

It doesn't bubble that far. Check the element's package page for the event's flags. Native events like submit are not composed, so they never cross a shadow root. Some events do not bubble at all.

A handle: function never runs

handle: takes the listener itself, and Quark calls it with the event, this being the element. When the value is something else, the console shows the listener's key followed by handle: needs a function or a list of functions, got <typeof>, naming the type it received. An empty value (null, undefined, preserve) is skipped without a message.

A quoted name. handle: "focusInput" is a string, and the console says so in its own words. Write the bare name: handle: focusInput.

A call. handle: focusInput() runs focusInput on every event, without the event, and hands Quark its return value as the listener. A function that returns nothing leaves no listener to call, and nothing is logged. Drop the parentheses, unless the call is a factory that returns the listener. event and target are not in scope there: the listener receives the event.

The name is wrong. handle: refers to an export of a module imported with @use. Check the export name and the as * / as name namespace.

An element ignores its attributes

Wrong attribute name. Attributes are kebab-case (targetRef → target-ref). Booleans are presence attributes (is-paused, not is-paused="false").

A form control shows a stale value

value: / checked: on <input>, selected: on <option> and content: on <textarea> also set the live property, so a control the user edited follows the rule whenever the rule writes. If it still looks stale:

  • The rule did not re-run. Nothing re-runs on typing; check the state attribute the rule is gated on.
  • It is a custom element. Quark writes only the attribute there; the element owns its own reflection.
  • It is a <select>. There is no value attribute; write selected: on the matching <option>.

prop("provision") returns nothing

  • The provider hasn't assigned the .provision DOM node property yet.
  • The selector doesn't match the provider's current state. provider-fetch[is-success] { $foo: prop("provision") } isn't matched while loading — that's a feature, not a bug.
  • The provision isn't a plain object or array. Providers must publish POJOs.
  • prop() reads the matched element only. To read an ancestor provider, publish it as a $binding from a rule matching the provider in a parent / is-global sheet.

prop("x") never re-runs

  • The object was mutated in place. Assign a new object — Quark observes assignments to element.x, not mutations inside it.
  • The browser changed the property without a JS assignment (an <input>'s value, open on <details>). Select on the reflected attribute / listen for the event.
  • The name isn't a literal. prop($name) reads but does not subscribe.

A remote template never renders

  • Wrong path in template-ref, or the fragment has more than one root element.
  • The include-content never became active: lazy-load waits for viewport entry, pre-fetch only warms, and conditional includes need is-active.
  • Check is-error on the element and the console for the fetch failure.

Debugging tools

  • The inspector is the debugger. Application state is the DOM. Watch attributes change in the Elements panel; that is your state timeline.
  • Nucleus DevTools First-party devtools that will upgrade your Chromium dev tools to assist in inspectablility of both Neutron elements and Quark rules. Install it from the Chrome Web Store.
  • Coding agents The extension's probe exposes selector-addressed JSON tools (diagnostics, state_snapshot, explain_attribute, …) that chrome-devtools-mcp discovers as a "Nucleus Stack" tool group, or any browser automation calls as __NUCLEUS_DEVTOOLS__.tools.*; the pane's Copy for AI button copies a one-file bug report. Works on production sites with no app changes. See Debugging with Agents.
  • Custom debugging Neutron.attachDevtools() is available.
  • QuarkRegistry is exposed on window in development. QuarkRegistry.findRules("bind-title") returns the rules that touch a selector; each rule tracks numberOfRuns.
  • is-error attributes on sheets, providers, forms, and includes reflect failures, and matching *-error events carry the detail.
  • Log level NucleusKit elements log only errors by default. import { KitLogger } from "@excom/nucleus-kit"; KitLogger.level = 2; adds warnings, such as a request that failed with an error status (0 silent, 1 errors, 2 warnings, 3 debug, 4 info). Quark logs through its own QuarkLogger, exported next to it, with the same levels set independently. The progressive entry exports KitLogger only.
  • Serialize the state. document.documentElement.outerHTML is a complete, shareable snapshot of the app at the moment of a bug. It will not include data provisions or Quark $variables.

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.