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

content-drawer

Slide-in drawers and sheets for nav menus, filters, confirmations, and side panels — any edge, with peek stages.

Features

  • Any edge Bottom (default), top, left, or right via from-side
  • Peek stages Full, half, or peek via open-stage
  • Command-driven --open / --close / --toggle from any <button command commandfor>
  • Dismissal Outside click + Escape via <dismiss-watcher>
  • Backdrop Valence.css dimmer — sibling [role="presentation"] / .tag-backdrop
  • Auto-dismiss disappear-after for toast-style confirmations
  • Singleton groups One open drawer per singleton-name
  • Layout modes Viewport sheet (default, position: fixed), .absolute (inside its parent), .relative, or .sticky
  • Scrubbable Wrap in gesture-handler: the sheet follows the finger via is-scrubbing + --content-drawer-open-progress

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/neutron@0.2.0/dist/index.umd.min.js"></script>
<script src="https://unpkg.com/@excom/content-drawer@0.2.3/dist/index.umd.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@excom/content-drawer@0.2.3/dist/index.css">
npm install @excom/content-drawer
HTML Imports JS / CSS Imports
<!-- import path to `node_modules` will depend on your build setup -->
<script type="module" src="/node_modules/@excom/content-drawer"></script>
<link rel="stylesheet" href="/node_modules/@excom/content-drawer">
import "@excom/content-drawer";
@import "@excom/content-drawer/index.css";
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
./content-drawer
  • types: ./dist/content-drawer.d.ts
  • import: ./dist/content-drawer.js
  • default: ./dist/content-drawer.js
./content-drawer.js
  • types: ./dist/content-drawer.d.ts
  • import: ./dist/content-drawer.js
  • default: ./dist/content-drawer.js
./content-drawer.min
  • types: ./dist/content-drawer.d.ts
  • import: ./dist/content-drawer.min.js
  • default: ./dist/content-drawer.min.js
./content-drawer.min.js
  • types: ./dist/content-drawer.d.ts
  • import: ./dist/content-drawer.min.js
  • default: ./dist/content-drawer.min.js
./content-drawer.umd.min
  • types: ./dist/content-drawer.d.ts
  • default: ./dist/content-drawer.umd.min.js
./content-drawer.umd.min.js
  • types: ./dist/content-drawer.d.ts
  • default: ./dist/content-drawer.umd.min.js
./index
  • types: ./dist/index.d.ts
  • import: ./dist/index.js
  • default: ./dist/index.js
./index.bundle.min.css
  • default: ./dist/index.bundle.min.css
./index.css
  • default: ./dist/index.css
./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.css
  • default: ./dist/index.min.css
./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

Usage

Put content inside <content-drawer> and invoke --open, --close, or --toggle on it — a native <button command commandfor>, or <event-handler command-name target-ref> when the invoker is not a button. The parent of an .absolute drawer becomes position: relative; overflow: clip, so give that parent a real size along the drawer's axis.

<button type="button" command="--toggle" commandfor="sheet">Toggle sheet</button>
<content-drawer id="sheet" class="absolute">
  <h2>Saved!</h2>
</content-drawer>
<event-handler class="tag-backdrop" role="presentation" target-ref="content-drawer:has(+ :scope)" command-name="--close"></event-handler>

The backdrop never reads the drawer's own variables: set --content-drawer-transition-duration, --content-drawer-transition-ease and --content-drawer-overlay-z-index on the parent, the backdrop or :root. After its slide-out a closed drawer is visibility: hidden, so a custom transition on it must keep visibility 0s <duration>.

API Reference

content-drawer

Attributes (5) Role — option is configurable, state is managed by the element (read-only), or hybrid which is both. Role Attribute Type Values Prop Default option disappear-after
Auto-close after this many seconds once opened — toast-style / transient confirmations. disappearAfter
number null
option from-side
Edge the drawer slides from. fromSide
string "bottom" | "top" | "left" | "right" "bottom"
option singleton-name
Shared group name. Opening one drawer closes others with the same name (singleton coordination). singletonName
string null
hybrid is-open
Open state. Toggle directly, or through the --open / --close / --toggle commands. For Escape / outside-click dismissal pair with <dismiss-watcher command-name="--close">. isOpen
boolean false
hybrid open-stage
How far the drawer opens: 0 full, 1 half, 2 peek. Unset = full. Set statically, or per open through data-open-stage on the --open / --toggle invoker. openStage
number "0" | "1" | "2" null
Provision (0) This provision property lives on the DOM node. Read / watch it with Quark’s prop("provision"), or listen for the neutron-provision event from app JS. Property Type
Events (5)
Dispatches — events that content-drawer fires and their default actions. Dispatch Default Action
A "default action" is subsequent logic executed by the element if e.preventDefault() is not synchronously called on the event.
Name content-drawer-closed
After close (is-open unset), detail is the drawer.

Type ContentDrawerClosedEvent
Name content-drawer-opened
After open (is-open set), detail is the drawer. When singleton-name is set it is also broadcast so drawers sharing that name close.

Type ContentDrawerOpenedEvent
Listeners — events that content-drawer listens for which will trigger subsequent actions. Listener Type Action
Commands — verbs content-drawer accepts as a command event (HTML Command API): from a <button command commandfor>, an <event-handler command-name>, or a CommandEvent. Command Action --close Closes the drawer (is-open unset). --open Opens the drawer (is-open set). data-* attributes on the invoker that name a declared prop (data-open-stage, data-from-side, …) are applied first; unknown, private and isOpen keys are ignored. --toggle Toggles open / closed. Reads the invoker's data-* like --open.
Recognized Elements (0) Child or descendant elements are recognized by content-drawer and relevant to its functionality. Element Relationship Required
Styles (25)
Classes — optional classes that change the appearance of content-drawer. Class Description .absolute position: absolute instead of the default fixed placement — a sheet inside its parent, which becomes position: relative; overflow: clip. .close Close control — empty .close / [rel="prev"] button or link (theme close icon). .relative In-flow mode: the drawer participates in layout and animates max-width instead of overlaying via position, pushing sibling content aside as it opens. Currently only affects from-side="left". .sticky position: sticky instead of the default fixed placement.
Variables — public CSS variables for theming content-drawer. Opt-out:
If you wish to opt-out of these styles on a case-to-case basis, use property all: revert-layer. Valence.css ships this as .unstyled and .unstyled-all for the entire tree.
Variable Syntax Default --content-drawer-closed <percentage> 101% --content-drawer-max-width <length> --content-drawer-open-full <percentage> 0% --content-drawer-open-half <percentage> 50% --content-drawer-open-peek <percentage> 75% --content-drawer-open-progress <number> var(--gesture-progress, 0) --content-drawer-overlay-z-index <integer> 2 --content-drawer-relative-max-width <length> 200px --content-drawer-transition-duration <time> 0.25s --content-drawer-transition-ease <easing-function> ease-out --content-drawer-z-index <integer> 3
Aliases — @custom-selector synonyms so custom elements can share semantic styles. Element aliases match the host tag / .tag-* class; state aliases match attributes (nested under the element alias). Alias Kind Matches :--content-drawer element content-drawer, .tag-content-drawer :--content-drawer--from-side state [from-side], [data-placement] :--content-drawer--from-side-bottom state [from-side="bottom"], [data-placement="bottom"] :--content-drawer--from-side-left state [from-side="left"], [data-placement="left"] :--content-drawer--from-side-right state [from-side="right"], [data-placement="right"] :--content-drawer--from-side-top state [from-side="top"], [data-placement="top"] :--content-drawer--is-open state [is-open], [aria-expanded="true"] :--content-drawer--is-scrubbing state [is-scrubbing][is-scrubbing], [data-scrubbing][data-scrubbing] :--content-drawer--open-stage-1 state [open-stage="1"], [data-open="1"] :--content-drawer--open-stage-2 state [open-stage="2"], [data-open="2"]
Release notes (2)

0.2.0

  • Update content-drawer so only an .absolute drawer styles its parent, as position: relative; overflow: clip (was every drawer, with overflow: hidden): a fixed or sticky drawer no longer clips its parent, and sticky descendants keep working
  • Update a closed content-drawer to be visibility: hidden once its slide-out ends, taking it out of the tab order and the accessibility tree: a custom transition on a closed drawer must keep visibility 0s <duration>, and a closed drawer shown in the layout needs visibility: visible

0.1.1

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

Examples

Peek stages

--open with data-open-stage on the button sets the stage — 0 full, 1 half, 2 peek. The exact heights of each stage are configurable through CSS variables.

Dismissal

<dismiss-watcher command-name="--close"> as the drawer's first child closes it on outside click or Escape / back gesture; the sheet gates its is-active on content-drawer[is-open] (with the inverse rule). Open with --open, not --toggle — an outside mouseup on the button closes first, the click then re-opens. Immediate next sibling [role="presentation"] / .tag-backdrop is the Valence.css modal dimmer; <event-handler command-name="--close"> on that node closes on click.

Side drawer

from-side slides from left / right / top instead of the default bottom.

Auto-dismiss

disappear-after closes the drawer after N seconds — useful for success toasts.

Singleton group

Drawers sharing singleton-name — opening one closes the other.

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.