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

quark

CSS-like orchestration for your HTML — bind attributes, render lists, and wire events without a component tree.

Quark is a derivative of CSS, written in CSS syntax. Selectors, nesting, declarations and comments are CSS's own; the few additions Quark makes, such as variables, expressions and at-rules of its own, stay compatible with that syntax, so anyone who can read a stylesheet can read a sheet. What differs is the runtime: a stylesheet paints, a Quark sheet writes State — attributes, content and variables on the elements it matches. Media queries, keyframes and the rest of the browser's style engine stay in your stylesheet.

Prefer <quark-sheet> for apps; use the Quark class when you need a programmatic host (tests, tooling).

Features

  • CSS-like sheets Selectors + nested rules that mutate the live DOM
  • $variables Scoped values that nest and resolve in expressions
  • JS writes element.quark.setProperty() hands values app JS already holds to the document as $variables
  • CSS variables Write --custom-props from state; style via var()
  • Content rendering content, template(), iterate(), dangerous-html()
  • Element properties prop("provision") reads Neutron provisions / any JS property, re-running on assignment
  • Events @on at-rules with delegation, key, timing and host options, plus prevent-default / stop-propagation
  • View transitions @view-transition commits a block's writes inside document.startViewTransition(), so CSS animates list changes, removals included
  • Delayed writes @delay 2000 { … } applies a block after a pause — flashes, toasts, undo windows — dropped if the rule stopped matching
  • Diagnostics @warn / @debug / @error report from a rule; the selector is the condition
  • Built-in modules @use "quark:math", quark:list, quark:map, quark:string, quark:date, quark:url, quark:util — pure helpers, imported like JS modules
  • Attribute helpers dataset, ariaset, class, none to clear
  • JS modules Pure functions from @use "/url", called in expressions
  • Scoped host Sheet + targets share a parent; updates follow DOM mutations
  • DevTools Quark.attachDevtools() reports rule applications and $variables to the Nucleus DevTools extension

Installation

This package is available in the NucleusKit. Or it can be used by itself:

CDN Package Manager
<script src="https://unpkg.com/@excom/kit-utils@0.3.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/quark@0.4.1/dist/index.umd.min.js"></script>
npm install @excom/quark
HTML Imports JS / CSS Imports
import { /* … */ } from "@excom/quark";
Peer dependencies (0)

Packages a consumer must install alongside this one. Workspace deps are bundled.

Package Version
View Dist Files

All exports:

.
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.js
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.min
  • types: ./dist/index.d.ts
  • import: ./dist/index.min.js
  • default: ./dist/index.min.js
./index.min.js
  • types: ./dist/index.d.ts
  • import: ./dist/index.min.js
  • default: ./dist/index.min.js
./index.umd.min
  • types: ./dist/index.d.ts
  • default: ./dist/index.umd.min.js
./index.umd.min.js
  • types: ./dist/index.d.ts
  • default: ./dist/index.umd.min.js
./language
  • types: ./dist/language.d.ts
  • import: ./dist/language.js
  • default: ./dist/language.js
./language.js
  • types: ./dist/language.d.ts
  • import: ./dist/language.js
  • default: ./dist/language.js
./language.min
  • types: ./dist/language.d.ts
  • import: ./dist/language.min.js
  • default: ./dist/language.min.js
./language.min.js
  • types: ./dist/language.d.ts
  • import: ./dist/language.min.js
  • default: ./dist/language.min.js
./language.umd.min
  • types: ./dist/language.d.ts
  • default: ./dist/language.umd.min.js
./language.umd.min.js
  • types: ./dist/language.d.ts
  • default: ./dist/language.umd.min.js

Usage

App authors almost always load Quark through <quark-sheet>:

<section>
  <quark-sheet>
    details[open] [bind-status] {
      content: "Open";
    }
    details:not([open]) [bind-status] {
      content: "Closed";
    }
  </quark-sheet>
  <details>
    <summary>Panel</summary>
    <span bind-status></span>
  </details>
</section>

Programmatic API (tests / custom hosts):

import { Quark } from "@excom/quark";
​
const quark = new Quark({
  src: `span { content: "four times two equals #{twice(4)}"; }`,
});
quark.register({
  sheetElement, // host = sheetElement.parentElement
  modules: { dfault: { twice: (n) => n * 2 } },
});
// …
quark.unregister();

Documentation

Syntax

  • Sheets & scoping — <quark-sheet>, @scope, is-global, what runs
  • Syntax — rules, declarations, literals, operators, if()

Selectors

  • Selectors — combinators and pseudo-classes, what is observed
  • Reactivity — when a rule re-runs, loops, timing
  • No reversion — write the inverse rule

Declarations

  • Declaration kinds — what a key does
  • Attributes — attributes, class / dataset / ariaset, form controls
  • Content — content, template(), iterate(), dangerous-html()
  • CSS variables — --custom-props

Values & expressions

  • Variables — $variables, cascade, unset, raising state
  • Writing from JS — element.quark.setProperty()
  • Values & keywords — none / preserve / unset, wipes and no-ops
  • Expressions — name resolution, operators, if(), lists and maps
  • Built-in functions — attr(), prop(), iterate(), event, …
  • Allowed methods — .toFixed(), .join(), …
  • Built-in modules — @use "quark:math", quark:list, quark:map, quark:string, quark:date, quark:url, quark:util
  • Element properties — prop("provision"), element

At-rules

  • At-rules — the ones that run
  • @use — JS modules, asynchronous work
  • @on — events, blocks, options
  • @dispatch / @command — outgoing events and commands
  • @view-transition — animated writes
  • @delay — deferred writes
  • @warn / @debug / @error — diagnostics from a rule

Runtime

  • JS API — Quark, whenSettled(), DevTools
  • Loop guard — runaway chains are cut
  • Limitations — beta limits and pitfalls

The grammar (EBNF, precedence, AST) is the quark-parser package's Language reference.

Examples

React to element state

Native element state drives content — no JS, no listeners:

List from a provider

prop("provision") pulls the fetch payload; iterate() renders a row per item and re-renders on every provision:

Call a module helper

<quark-sheet>
  @use "/helpers.js" as *;
​
  #out { content: formatPrice($amount); }
</quark-sheet>

Hand a value from app JS

App JS that already holds a rich value (feature flags, a messages dictionary) hands it to the document with element.quark.setProperty(), and every rule reading that $variable below the element re-runs. See Writing from JS.

Release notes (7)

0.4.1

  • Fix a @use module with a top-level await on Safari: sheets that import one module while it loads now share that load. WebKit resolved the later imports before the module had finished, so its functions ran too early on a page's first load
  • Fix the iterate() description for a non-collection, the Programmatic API samples (modules go to register()) and how the Diagnostics page says to raise the log level

0.4.0

  • Add hydration of prerendered pages: iterate() rows are adopted by their q-key, content painted by template() or dangerous-html() is kept when its source matches, and a $binding nobody has written yet keeps the server's paint until hydration ends
  • Improve paints when two rules write the same attribute, class string or text content of one element in one pass: only the later one writes, so the element never shows the replaced value and a replaced write starts no view transition
  • Fix an issue where a rule rewrote an attribute whose value already read the same as text (data-n: 0 over data-n="0")
  • Fix an issue where Quark.whenSettled({ timeout: Infinity }) resolved "timeout" at once: it now waits with no cap
  • Fix an issue where a @warn or @error whose value was a preserve reported nothing later: it now stays silent and reports on a later run that has a value

0.3.0

  • Quark.meter exposes the engine's work counters for complexity snapshots
  • Fix the published type declarations: index.d.ts pointed at a folder that is not in the tarball, so consumers got any for named re-exports and missing-member errors for export * instead of the real types

0.2.0

  • Remove promise support from content:. A promise that a module function returns to content: is refused with a console error and the content stays as it was; return a value or a node instead. template(), iterate() and @view-transition (until: …) wait as before.
  • Update handle: in @on to name the listener: event and target are not in scope inside a handle: expression. A call in handle: runs on every event and must return the listener, which receives the event with this being the element.
  • Add the key names Space / Spacebar and plus to key: in @on ("Shift+Space"); a key: holding only spaces now warns and names Space
  • Add absolute http(s) URLs to @use: the module loads from that URL (another origin needs CORS), and any other scheme (data:, blob:) is refused with a logged error
  • Add QuarkLogger to the exports of @excom/quark: QuarkLogger.level = 2 shows the warnings Quark logs
  • Improve element insertions so they run rules only for the inserted elements, plus rules that depend on children or sibling position, instead of re-running every rule over the parent's whole subtree. A dialog or popover opening under a sheet's host no longer re-applies its rules (or restarts their @delay timers).
  • Add a console warning when handle: evaluates to a value that is not a function
  • Fix an issue where a rule reading a $binding or prop() did not apply its value again when an attribute or child change made it match again, so an inverse rule left the old value in place. Where two rules reach the same key, the later one now wins for bindings, as it does for literals.
  • Fix an issue where values indexed by attr(), such as $names[attr("data-key")], did not update when the attribute changed

0.1.3

  • Declare the MIT license in package.json (was ISC), matching the repo LICENSE and every other package

0.1.2

  • Fix @delay blocks inside a bare :scope rule (or a :scope > … rule) never firing: the re-check at fire time handed the empty host compound to matches(), which throws on an empty selector

0.1.1

  • Ship only dist (and declared extras) in the npm tarball; drop build logs, tests and sources
View Source

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.