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

Styling

Style the state, not the script. CSS in the Nucleus Stack can read the same attributes Quark does, so visuals and behavior stay aligned without a line of glue.

Valence.css

Valence.css is the stack's semantic, classless CSS: Pico's elegance, remixed so custom elements receive the same treatment as native tags, with other minor additions.

@import "@excom/valence/basic.css";

nucleus-kit/basic.css already includes Valence.css plus every element's own styles, so loading NucleusKit means you are already styled.

  • Semantic tag aliases — article, button, dialog, table, forms, and typography look right with no classes at all.
  • Role and class forms — every tag alias is also matched by an ARIA role and a .tag-* class, so a custom element can borrow a native look: <event-handler role="button"> or <my-card class="tag-article">. Prefer the role; semantics come along for free.
  • Molecules — cards, dropdowns, modals, nav, progress, popovers, and tooltips assembled on top of the tag aliases.
  • Schemes — light by default, dark via prefers-color-scheme, or force either with <html data-scheme="dark">.
  • Themes — basic stays Pico-faithful; further themes share the same tokens so they can be swapped without touching markup.
  • Opt-outs — .unstyled strips Valence.css from an element, .unstyled-all from its subtree, .unanimated* drops motion. Reach for these wherever framework styles would fight you.

Tokens

Design tokens are CSS custom properties under the --v-* namespace: --v-primary, --v-spacing, --v-border-radius, --v-font-family, and the rest. Override them at :root, on a scheme, or on any subtree.

:root { --v-primary: #2a6; --v-border-radius: 0.75rem; }
[data-scheme="dark"] { --v-primary: #5c8; }
aside { --v-spacing: 0.5rem; }

Use tokens in your own CSS so themes and schemes apply to your views automatically.

Style the state

Classes are for static flavor only. details.accordion versus details.dropdown is a class. .is-open is not — that is state, and state belongs in an attribute the whole stack can see.

Hook native state first, then ARIA, then data-*:

details[open] > summary { font-weight: 600; }
input:not(:valid) { border-color: crimson; }
[aria-current] { text-decoration: underline; }
li[data-done] { opacity: 0.6; text-decoration: line-through; }

Element state attributes are the same hooks:

provider-fetch[is-loading] { opacity: 0.5; pointer-events: none; }
provider-fetch[is-error]::before { content: "Something went wrong."; }
spa-a[is-active] { color: var(--v-primary); }

The payoff: one attribute drives CSS, Quark, and assistive technology together, and it is visible in the inspector.

Element CSS

Every element ships its styles next to its script and documents its CSS hooks on its package page: exposed variables, style classes (content-tabs.underline, content-tabs.file-tabs), and aliases. Override with your own selectors; nothing is locked behind a shadow root.

Write modern CSS in your stylesheets. Nesting, @scope, :has(), color-mix(), container queries, and view transitions are all fair game there. A Quark sheet is a derivative of CSS with a different runtime, so media queries, keyframes and the rest of the style engine stay in the stylesheet. Quark observes :has() too; interaction pseudo-classes (:hover, :focus) stay CSS-only (first run in a sheet).

Quark and CSS together

Quark can write CSS custom properties from state, and CSS consumes them with var(). Use this for values a selector cannot express: computed colors, percentages, live theming.

[bind-progress] { --progress: "#{($done / $total * 100)}%"; }
[bind-progress]::after { width: var(--progress); }

Quote CSS literals in Quark ("#ccc", "10px") — the value is an expression, not CSS grammar.

Animation

State describes only the present, so an animation — a description of change over time — has no place in the document. Declare the destination state and let CSS own the motion:

  • Transitions on state attributes: [data-open] { translate: 0; transition: translate 200ms; }
  • View transitions between routes: spa-manager batches route changes into one document.startViewTransition(), and spa-route / spa-a reflect is-active / was-active so you can style enter and exit. last-move on the manager (push / back / forward) picks direction-aware transitions.
  • Card expansion and shared-element effects come from CSS view-transition-name keyed to those same attributes.
  • List and content changes from Quark: wrap the writes in @view-transition (types: "…") { … } and style the result. view-transition-name: match-element plus a view-transition-class on rows gives each row its own group, so ::view-transition-new(.row):only-child animates rows that enter and ::view-transition-old(.row):only-child rows that leave, and :root:active-view-transition-type(…) scopes the rules to that change. Keyed iterate() keeps rows alive, so moved rows slide instead of fading. Where match-element is not available, let the sheet name the rows (--vt-name: "row-#{item.id}") and read it in CSS (view-transition-name: var(--vt-name)). The View Transitions example shows a card expansion, a page slide, a list reflow and a text crossfade built this way.

Quark writes are batched and not frame-aligned, so it is the wrong tool for per-frame values. Reach for CSS or the Web Animations API instead.

Views

Each view owns its stylesheet, loaded by a <link> at the top of the fragment. Scope view CSS to the view's root element and keep shared layout in one site-wide stylesheet. See Building Views.

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.