- Roadmap
- How releases are planned
- Known Issues
- CwElement’s reflected-attribute “morph fix” mishandles null-valued properties
- v1.1 — Forms & Input Essentials
- <cw-combobox>
- <cw-date-picker> and <cw-time-picker>
- <cw-pagination>
- Branding refresh
- v1.2 — Data Display
- <cw-data-table> and <cw-data-table-column>
- v1.3 — Site Navigation & Page Chrome
- “CW” shorthand logo mark
- v1.4 — Shoelace Migration Tooling
- Migration command suite
- Distribution & documentation
- Future scope: the permanent upgrade path, not just the Shoelace exit
- v1.5 — Data Display, Continued & Interaction Primitives
- <cw-accordion> and <cw-accordion-item>
- <cw-avatar-group>
- <cw-stepper> and <cw-stepper-item>
- <cw-segmented-button>
- Fixed: cw-radio-group had no disabled property
- <cw-popover>
- Breadcrumb overflow
- v1.6 — Page Content & Layout
- <cw-picture>
- <cw-video-embed>
- Layout primitives
- <cw-section>
- <cw-grid>
- <cw-stack>
- Layout work plan
- v1.7 — Media Players
- <cw-audio-player>
- <cw-video-player>
- v1.8 — Performance, Theming & Polish
- Virtualized rendering
- <cw-file-upload>
- Theming improvements
- Accessibility & RTL audit
- Re-enable Firefox in the test suite
- Enforce JSDoc comments via ESLint
- v1.9 — Coach Marks, Notifications & Data Viz Primitives
- <cw-coach-mark>
- <cw-timeline>
- <cw-textarea> pattern validation
- Investigated, not pursued (yet)
- Beyond v1.9
Roadmap
Cordwainer Elements 1.0 launched as a hard fork of
Shoelace, with all 58 components renamed and re-themed under the cw- prefix. Since then, we’ve shipped
16 new components: <cw-combobox>, <cw-date-picker>, <cw-time-picker>, <cw-pagination>, <cw-data-table>, <cw-header>, <cw-logo>, <cw-primary-navigation>, <cw-secondary-navigation>, <cw-footer>, <cw-social-link>, <cw-accordion>, <cw-avatar-group>, <cw-popover>, <cw-segmented-button>, and <cw-stepper>. This page outlines what we’re planning to build during the v1.x release line — new components,
enhancements, and infrastructure work — roughly in priority order.
This roadmap reflects current thinking, not a contract. Priorities can shift based on real-world usage, community feedback, and upstream changes to the web platform. If something here matters to you — or something’s missing — let us know via Issues.
Note on sequencing (August 2026): v1.3 has been reprioritized ahead of some previously-planned work. Building a real site on Cordwainer Elements surfaced a hard gap — no way to assemble page navigation, branding, or a footer — so that work has moved up, and the components originally scoped for v1.2/v1.3 that aren’t part of that gap have shifted down to v1.5. A Shoelace-to-Cordwainer migration toolkit has also been added as v1.4, ahead of that shifted component work. A dedicated Media release (v1.6) was also split out afterward, rather than scattering image/audio/video components across whichever release happened to be convenient.
Update (September 2026): v1.4’s distribution question is resolved — the migration toolkit
ships as its own standalone package,
@cordwainer/cw-migrate, following the same pattern as @cordwainer/cw-animations and
@cordwainer/cw-localize (its own GitLab repo, independently versioned, developed alongside this
one in the same dev environment but not part of this repo’s git history). Deciding on a real package name
and a real npx entry point surfaced a documentation-site gap: cw-animations and
cw-localize are runtime libraries most users meet through this site’s Getting Started
pages, but neither has ever had a docs page here, and cw-migrate — a CLI tool, not a library
import — doesn’t fit that model either. Resolved: cw-migrate gets its own new
Tooling nav section here (it’s a command people run, not something they import), while
cw-animations and cw-localize each get a page under
Getting Started, next to Themes/Customizing/Localization, since that’s where users already
encounter them. All docs for all three continue to be authored in this repo’s Eleventy site, since
that’s what builds cordwainer.dev — none of them need or get their own docs site.
Update (October 2026): v1.6 has been re-scoped. The same real site that moved v1.3 up hit a
second hard gap: there’s no way to lay out a page — columns, a centered content width, consistent spacing
between blocks — without hand-written CSS, which a page builder driven by the Custom Elements Manifest can’t
offer at all. Layout primitives (<cw-section>, <cw-grid>,
<cw-stack>) join <cw-picture> and <cw-video-embed>,
both already built, in a renamed v1.6 — Page Content & Layout. The self-hosted media players
(<cw-audio-player>, <cw-video-player>), which share an engine and both
still have open scoping questions, move to their own v1.7 release, and every release after that shifts down
one version.
How releases are planned
Each v1.x minor release is themed around a category of gaps in the current component set. Every new component that ships will, at minimum:
-
Be built on the same
CwElement/CwFormControlfoundations as existing components, with full TypeScript types - Ship with React, Vue, Angular, and Svelte wrapper support (see Frameworks)
- Be translated across all 29 supported locales
- Include a documentation page with live, editable examples
- Be reviewed against our Accessibility Commitment (keyboard support, ARIA, screen reader testing)
Known Issues
Bugs in shared infrastructure, discovered along the way, that don’t belong to any one component’s own scope.
CwElement’s reflected-attribute “morph fix” mishandles null-valued properties
Found while building
<cw-header> (v1.3).
CwElement.willUpdate() (src/internal/cw-element.ts) records the first non-null
value any reflect: true property ever takes, then force-resets the property back to that
recorded value any time it’s later set to null. This exists to protect a reflected default from
being accidentally wiped out (e.g. by external DOM morphing removing the attribute). Every reflected
property that existed anywhere in the library before this was found is a boolean (open,
current, disabled, etc.) — booleans never become null, so the bug had
no way to surface until a non-boolean case came along.
<cw-header>’s activePanel ('utility' | 'search' | 'notification' | 'user' | 'cart' | null
— which action panel is open, if any) was the first reflected property in the library where
null is a normal, frequently-reached runtime value rather than an error/removal case. The
“morph fix” recorded whichever panel opened first as the permanent “initial” value, then silently reverted
activePanel back to it every time the panel was closed — the close action never actually took
effect.
Workaround applied: <cw-header>’s activePanel is declared
@property({ attribute: false }), opting out of attribute reflection entirely and sidestepping
the bug (see src/components/header/header.component.ts). No changes were made to
CwElement itself.
Still open: any future property that is both reflect: true and legitimately
null-valued at runtime will hit the same bug. A real fix likely needs
willUpdate() to distinguish “this property was reset via external attribute removal” from “this
property was set to null programmatically as a normal value” — treating only the former as the
case worth protecting against — rather than reacting to any transition to null regardless of
cause.
v1.1 — Forms & Input Essentials
The most-requested gaps in web component libraries are autocomplete/combobox, date and time selection, and pagination. These are foundational form/input pieces that a lot of real applications need before they can fully adopt the library.
<cw-combobox>
A filterable, searchable input that combines <cw-input> with a
<cw-popup>-based listbox:
- Type-ahead filtering of options as the user types
- Optional free-text entry vs. constrained-to-list selection
- Multi-select mode using
<cw-tag>chips for selected values - Support for asynchronous option loading (e.g., remote search)
<cw-date-picker> and <cw-time-picker>
Calendar- and clock-based pickers that integrate with the existing form-association internals shared by
<cw-input>, <cw-select>, etc.:
- Single date, date range, and time selection modes
- Min/max date constraints and disabled-date callbacks
-
Localized month/day names and date formats via
cw-localize, reusing the locale data already used by<cw-format-date> - Keyboard navigation (arrow keys, Page Up/Down for month, Home/End)
<cw-pagination>
A standalone page-navigation control for lists, search results, and (in v1.2) data tables:
- Configurable page size, sibling/boundary page counts, and ellipsis collapsing
- Works as a controlled component (emits page-change events; doesn’t own the data)
Branding refresh
The current logo is a placeholder from the fork. During v1.1, Matt Hand will design and roll out the new Cordwainer Elements identity — logo, favicon, docs header, and social preview images. (No engineering dependency on the rest of the v1.1 component work; can land independently.)
v1.2 — Data Display
With input essentials in place, the next gap is presenting structured data. This release is narrowed to a single component:
<cw-data-table> and <cw-data-table-column>
A slot/template-driven table component:
- Declarative column definitions with custom cell templates via slots
- Sortable columns (client-side initially; emits events so consumers can wire up server-side sorting)
- Row selection (single and multi, with a
<cw-checkbox>column) - Pairs with
<cw-pagination>from v1.1 - Virtualization for large datasets is deferred to v1.8 — v1.2 targets correctness and API shape first
<cw-accordion>, <cw-avatar-group>, and
<cw-stepper> were originally scoped for this release too — they’ve moved to v1.5 to make
room for the navigation/page-chrome work and Shoelace migration tooling below.
v1.3 — Site Navigation & Page Chrome
Building a real site on Cordwainer Elements surfaced a gap: the library has no way to assemble the navigation, branding, and footer that essentially every site needs. This release is inspired primarily by the Red Hat Design System’s navigation category, which treats page chrome as a first-class, richly-decomposed set of components rather than a single generic menu — a departure from the Shoelace-derived “everyday primitives” approach the rest of the library follows.
Scope for each component below will be defined individually as it’s picked up. Site search itself doesn’t
need a new component here — <cw-combobox> (v1.1) already covers it. Initial list:
-
<cw-primary-navigation>— top-level site navigation -
<cw-secondary-navigation>— second-level nav for linking related pages beneath primary nav -
<cw-logo>— branding mark component -
<cw-header>— page header region -
<cw-footer>— site footer
Open scoping question: RHDS itself folds branding directly into its primary-nav component
and has no separate header element at all. Worth deciding, when <cw-header> is scoped,
whether it’s a layout wrapper composing <cw-logo> +
<cw-primary-navigation>, or whether that slot belongs directly on
<cw-primary-navigation> instead, matching RHDS more closely.
“CW” shorthand logo mark
A compact wordmark — just “CW” — for spots the full logo doesn’t fit: favicon, browser tab, small graphic
contexts. Distinct from <cw-logo> above, which carries the full brand mark; this is the
icon-sized fallback it needs.
v1.4 — Shoelace Migration Tooling
Cordwainer Elements is a rename-and-retheme fork of Shoelace: same component shapes and attributes,
different tag names (sl- → cw-), CSS custom property namespace (--sl-
→ --cw-), custom event names (sl- → cw-), and TypeScript class/type
names (Sl* → Cw*) — including for anyone who’s built their own components on top
of Shoelace’s. Today, adopting Cordwainer in an existing Shoelace project means finding and renaming all
four by hand across every file type in the project. This release ships
@cordwainer/cw-migrate, a standalone CLI package that automates it — installed and invoked directly against a target project
(npx @cordwainer/cw-migrate <path>), not something added as a dependency of that project.
Quality bar for this release (in place of the component checklist above): extensive
automated test coverage for every script — fixture projects exercising the parser edge cases described below
(mismatched tag/property prefixes, extends-based consumer components, ambient type
augmentation, etc.) across each supported file type — plus a working --dry-run mode that
changes nothing and only reports, and a clear rollback path (git is the assumed safety net; the tool should
refuse to run against a dirty working tree unless --force is passed).
TODO: research public open-source projects built on Shoelace that could serve as a
real-world test case for the migration toolkit — a live, previously-unconverted codebase to run
cw-migrate detect/cw-migrate --dry-run against, beyond the synthetic fixture suite
above. (matthand.com does not use Shoelace, so it isn’t a candidate here.)
Update (September 2026): the migration command suite below is fully implemented and covered
by automated tests, including the parser edge cases called out in the quality bar above.
cw-migrate is releasing as 1.0.0-alpha next, followed by the real-world testing
this section’s TODO calls for — running the alpha against actual Shoelace projects and confirming the
migrated output stays healthy — before promoting it to Beta and then Stable. Documented for users on its own
Tooling page.
Update (September 2026): the real-world testing called for above is done.
cw-migrate ran end-to-end against
GoogleChrome/chromium-dashboard
(chromestatus.com), a production Lit + TypeScript Shoelace codebase — the exact kind of test case this
section’s original TODO was looking for. That run surfaced and fixed four gaps: elements wasn’t
scanning .ts/.js files at all (so sl-* tags inside Lit
html tagged templates, the majority of that project’s Shoelace usage, went untouched), no
command renamed sl-* CSS type selectors, plain style="..." attributes inside Lit
templates weren’t covered by the token rewrite, and sl-* selector strings passed to
.querySelector/.querySelectorAll/.closest/.matches
weren’t renamed. With those fixed, the migrated result installs and builds cleanly. On the strength of that
result, cw-migrate shipped straight to 1.0.0 stable rather than through a separate
Beta step. Sample migration branch and write-up:
matthandus/chromium-dashboard-migrate-shoelace-to-cordwainer#1.
Migration command suite
One CLI package, @cordwainer/cw-migrate, exposing one subcommand per rename
domain (rather than per file type, since each domain needs different tooling to do safely) plus a
default orchestrator command:
-
cw-migrate detect <path>— scans the target project and reports every Shoelace element, token, event, and TypeScript class/type reference found, with file/line references. Writes nothing. Needs to look beyond string grepping for the TypeScript layer below (aclass MyButton extends SlButtonwon’t contain the stringsl-anywhere). Always run first (and run implicitly as the first phase of the default orchestrator command below), so the other commands consume its scan output rather than re-scanning. -
cw-migrate elements <path>— renames custom element tags (<sl-button>→<cw-button>, opening and closing tags) across plain HTML, JSX/TSX, Vue SFC templates, Angular templates, and Svelte files. -
cw-migrate tokens <path>— renames CSS custom properties (--sl-color-primary-600→--cw-color-primary-600, etc.) across CSS/SCSS/LESS files, inlinestyleattributes, and CSS-in-JS. -
cw-migrate events <path>— renames custom event names (sl-change→cw-change) inaddEventListenercalls and framework event bindings (@sl-changein Vue,(slChange)in Angular,onSlChangeprops in the React wrapper). Easy to miss since it’s neither a tag nor a CSS token — its own namespace, so its own command. -
cw-migrate typescript-refs <path>— renames Shoelace-exported class/type identifiers (SlButton→CwButton,SlChangeEvent→CwChangeEvent, etc.) everywhere they’re referenced, not just imported —extendsclauses on consumers’ own composite components, type annotations, generic constraints, and the ambientHTMLElementTagNameMap/JSX.IntrinsicElementsglobal augmentation these libraries ship (a paired rename: the string key and the type value both change together, or the map ends up mismatched). Covers.ts,.tsx, and.d.tsfiles. This is the layer that matters most for anyone who’s built their own components on top of Shoelace’s, rather than just used Shoelace’s directly in markup. -
cw-migrate imports <path>— updates JS/TS import specifiers (module paths), CDN<script>/<link>URLs, and framework wrapper package imports. Identifier renaming itself lives intypescript-refsabove, since that needs whole-file/whole-project reference tracing anyway, not just the import line. -
cw-migrate package-json <path>— swaps the@shoelace-style/shoelacedependency for@cordwainer/cw-elementsat the target version. -
cw-migrate <path>(no subcommand, the default/orchestrator command) — runs all of the above in order, supports--dry-run, and prints a summary of what would change or did change. Each domain remains directly runnable on its own (cw-migrate elements <path>) for anyone who wants to convert incrementally rather than all at once.
Implementation note: naive find-and-replace is unsafe here — sl-button and
sl-button-group need to be told apart, and a closing tag has to match its opener. Each command
should use a real parser for its domain instead of regex: an HTML/template parser (e.g. parse5)
for elements, PostCSS for tokens, and a type-aware AST tool (e.g.
ts-morph, using the actual TypeScript language service rather than syntax-only parsing) for
events and typescript-refs — the latter especially, since a text-only replace of
SlButton could touch a comment or string literal that shouldn’t change, and won’t reliably
catch every real reference the way symbol resolution will.
Distribution & documentation
Ships as its own standalone package,
@cordwainer/cw-migrate
— its own GitLab repo and its own npm release cadence, independent of @cordwainer/cw-elements’s
versioning, following the same standalone-package precedent as @cordwainer/cw-animations and
@cordwainer/cw-localize. It is invoked directly against a target project via
npx @cordwainer/cw-migrate <path> — never added as that project’s own dependency, since
its job is done once the rewrite is committed.
Despite living in its own repo, its documentation is authored here, in this site’s Eleventy source, under a
new Tooling nav section (docs/pages/tooling/cw-migrate.md) — this Eleventy
site is what builds cordwainer.dev, and there’s no reason for a second docs site just because the code lives
in a second repo. This also prompted moving cw-animations and cw-localize —
previously undocumented here — into pages under Getting Started, since users meet both of
those through pages already there (Themes, Customizing, Localization) rather than as standalone products the
way cw-migrate is.
Future scope: the permanent upgrade path, not just the Shoelace exit
@cordwainer/cw-migrate isn’t retired once the Shoelace migration work above ships and the
initial wave of conversions is done. The same package becomes the permanent home for codemods that ship
alongside future Cordwainer major version bumps — the same role tools like
react-codemod or Angular’s ng update schematics play for their own ecosystems. A
v2 breaking change (a renamed attribute, a restructured slot, a removed component) gets its own codemod
shipped through cw-migrate, rather than leaving consumers to hand-edit their way through every
major-version changelog. This is also why the package was named @cordwainer/cw-migrate rather
than something Shoelace-specific like @cordwainer/migrate-from-shoelace in the first place —
the general-purpose name already anticipated this.
Practically, this means the Migration command suite above should be built as
v1.4’s concrete, shipped codemods, not the package’s entire permanent surface area: the
underlying per-domain rewrite engines (the parse5/PostCSS/ts-morph infrastructure
behind elements/tokens/events/typescript-refs) should be
structured so a later, unrelated codemod can reuse them rather than reimplementing template/CSS/AST
traversal from scratch. Concretely deferred to whenever the first breaking Cordwainer major version is
actually being planned (no real breaking change exists yet to design against, so this is intentionally not
scoped further now):
-
A command shape for version-targeted codemods — e.g.
cw-migrate upgrade --from 1 --to 2 <path>, versus one subcommand per specific change (cw-migrate codemod <name> <path>), versus something else entirely. -
Whether
detect/--dry-run/the dirty-working-tree refusal (all designed around the one-time Shoelace exit) still make sense unchanged for a codemod someone might run repeatedly across incremental version bumps. -
How a codemod’s applicability gets scoped to “only the projects actually affected by this specific change”
rather than the Shoelace suite’s simpler “affected if any
sl-/--sl-/Sl*reference exists at all.”
v1.5 — Data Display, Continued & Interaction Primitives
The remaining Data Display components originally scoped for v1.2, plus the general-purpose interaction primitives originally scoped for v1.3, land here once v1.3’s navigation work and v1.4’s migration tooling ship.
Update (September 2026): v1.5 has shipped. All five components scoped below —
<cw-accordion>/<cw-accordion-item>,
<cw-avatar-group>, <cw-stepper>/<cw-stepper-item>,
<cw-segmented-button>, and <cw-popover> — along with
<cw-breadcrumb> overflow collapsing and the cw-radio-group
disabled fix documented below, are in the 1.5.0 release. See the
Changelog for the full 1.5.0 entry.
<cw-accordion> and <cw-accordion-item>
A multi-panel collapsible group, distinct from the single-panel <cw-details>:
- Single-expand (“only one panel open”) and multi-expand modes
- Smooth expand/collapse animation using
@cordwainer/cw-animations - Same icon/slot conventions as
<cw-details>for consistency
<cw-avatar-group>
Stacked, overlapping avatars with an overflow ”+N” indicator, built on the existing
<cw-avatar>:
- Configurable max visible count and overlap amount
- Overflow indicator can be a
<cw-tooltip>-triggered list of remaining members
<cw-stepper> and <cw-stepper-item>
A step indicator for multi-step forms, checkouts, and onboarding flows:
- Linear and non-linear (jump-to-step) modes
- Visual states for completed, active, disabled, and error steps
- Orientation support (horizontal/vertical)
<cw-picture> was also added here originally — it moved to v1.6 below.
<cw-segmented-button>
A single-select, mutually-exclusive button group — visually similar to
<cw-button-group> but behaves like <cw-radio-group> (single selection,
form-associated value).
Fixed: cw-radio-group had no disabled property
Discovered while building <cw-segmented-button> above, which runs the same shared
form-control test suite (runFormControlBaseTests) <cw-radio-group> does.
That suite had two tests — checking that checkValidity()/reportValidity() don’t
emit cw-invalid for a disabled control with a custom validity error — permanently skipped for
cw-radio-group via if (tagName !== 'cw-radio-group'), with a
// TODO: As soon as CwRadioGroup has a property disabled this condition can be removed comment
already sitting in the test file.
cw-radio-group implemented CwFormControl (which marks disabled as
optional) but never declared the property — setting radioGroup.disabled = true from outside
code silently did nothing.
Fixed within v1.5: cw-radio-group now has a reflected disabled property that gates
checkValidity()/reportValidity() (excluded from constraint validation, matching
<cw-segmented-button>) and blocks click/keyboard interaction, and — going further than
<cw-segmented-button> does — sets disabled on every child
<cw-radio>/<cw-radio-button> too, so each one gets its own
already-correct disabled styling rather than just looking disabled from the outside. A radio that was
individually disabled beforehand stays disabled after the group is re-enabled. The now-unnecessary test
exclusion has been removed.
<cw-popover>
A general-purpose popover built on the browser’s native
Popover API, complementing the existing <cw-popup> (low-level positioning primitive),
<cw-dropdown> (menu-focused), and <cw-tooltip> (hover hints):
- Anchor positioning reusing
@floating-ui/dom(already a dependency) - Light-dismiss behavior (click outside / Escape) for free
- Good fit for rich content like forms or previews that don’t belong in a tooltip or dropdown
Breadcrumb overflow
Enhance <cw-breadcrumb> to collapse long paths into an ellipsis with an overflow menu,
for deeply nested hierarchies (file systems, multi-level categories).
v1.6 — Page Content & Layout
Everything a content page is assembled from that the library doesn’t have yet: responsive images, embedded video, and the layout primitives that arrange them. Originally a Media-only release; re-scoped in October 2026 (see the note at the top of this page) so the two finished media components ship alongside the layout work rather than waiting on the self-hosted players, which moved to v1.7. None of the components below are inherited from Shoelace.
Update (October 2026):
<cw-video-embed> and
<cw-picture> are built and published in
1.6.0-alpha.2. Of the layout primitives planned below,
<cw-grid> is published in 1.6.0-alpha.3,
along with the spacing-token helper from the foundations step, and
<cw-stack> is published in 1.6.0-alpha.4.
<cw-section> and the
--cw-layout-width-* tokens are built (not yet published), with both of its scoping questions
decided below, and a Page Layout guide shows all three working
together. Changelog entries for all of v1.6 are written when 1.6.0 goes stable, not per alpha.
<cw-picture>
A responsive image component wrapping the native <picture>/srcset machinery
— a good picture pattern is easy to get wrong by hand:
-
Multiple resolutions via
srcsetdensity descriptors (1x/2x/3x) for different device pixel ratios -
Multiple sizes via width-descriptor
srcsetplus asizesattribute, so the browser picks the right image before layout, not after -
Art direction — different crops or compositions per breakpoint via slotted
<source media="">elements, not just a resized version of the same image -
Format negotiation — slotted
<source type="">elements so the browser can pick AVIF/WebP with a JPEG/PNG fallback automatically -
Explicit
width/height(oraspect-ratio) required or defaulted, to reserve layout space and avoid content shift while loading -
Lazy loading passthrough (
loading="lazy"), plus an optional placeholder (blurred low-quality image or solid color) shown until the real image loads -
Decorative-image handling (
alt="") kept distinct from meaningfulalttext, so accessibility isn’t an afterthought -
Optional
captionslot below the image, exposed to assistive devices as a figure named by its caption
Resolved scoping question: <cw-picture> supports both — declarative
attributes (src/srcset/sizes/aspect-ratio) as the
common-case baseline, and slotted <source> elements for full
art-direction/format-negotiation control. A native <picture> only honors
<source> elements that are its direct children, and ignores ones that are merely
slotted into it (confirmed in both Chromium and WebKit), so <cw-picture> mirrors its
light-DOM <source> children into the <picture> in its shadow root, and
keeps them in sync as they’re added, removed, or changed. For accessibility, alt is the
meaningful text and a separate decorative attribute renders alt=""; if neither is
set it falls back to an empty alt and warns in the console, so a forgotten description never
looks like an intentional one.
<cw-video-embed>
Most sites want both this and the self-hosted <cw-video-player> (v1.7) at different
times. This is a lighter, facade-pattern component for embedding third-party video (YouTube, Vimeo, etc.),
genuinely different from <cw-video-player> rather than a smaller version of it: it wraps
someone else’s player via iframe and can’t touch its internals, so all it can offer is lazy-loading, not
custom controls.
-
Lazy-load facade: a static thumbnail image displays until the user clicks, at which point the real embed
(an iframe) activates — avoids loading a third-party player’s JS and tracking scripts until someone
actually wants to watch, inspired by the pattern
Red Hat Design System’s
rh-video-embeduses -
Works generically with any iframe-embeddable provider rather than baking in provider-specific logic — the
embed markup itself is supplied by the consumer (e.g., inside an inert
<template>, activated on click) - Optional caption slot below the embed
Layout primitives
Three elements that arrange other content: <cw-section> sets a page band’s width and
spacing, <cw-grid> lays its children out in columns, and
<cw-stack> spaces its children vertically. They nest — a section holding a grid of stacks
covers most content pages.
Red Hat Design System
has no layout elements; it documents layout as a grid foundation applied with CSS, and Web Awesome ships
utility classes (wa-stack, wa-grid, …). Cordwainer ships elements instead, because
a CSS class never appears in the Custom Elements Manifest: tools that build pages from the manifest, such as
SDC generators for Drupal Canvas, can only offer what’s an element. Carbon’s
<cds-grid>/<cds-column> and
Every Layout’s custom-element primitives are the precedent.
Shared rules
- Presentational only. No form association, no events, no translatable strings — the 29-locale requirement has nothing to apply to. Framework wrappers are still generated.
- One default slot, no wrappers. Children are slotted directly, so each child is the layout item.
-
Spacing by token name. Spacing attributes take a spacing token name (
3x-smallthrough4x-large) and resolve tovar(--cw-spacing-*). They’re declared as string-literal unions, so the manifest exposes them as an enum (a dropdown in tools that read it) rather than free text. An unknown value falls back to the default and warns in the console. -
Container queries, not viewport media queries. Each component responds to its own width,
so a grid inside a narrow sidebar collapses even on a wide screen.
<cw-footer>already works this way. The trade-off: each host setscontainer-type: inline-size, which stops it from sizing to its content, so these components belong in block contexts or stretched flex/grid items. The docs say so. - Thresholds are CSS custom properties. Collapse points have defaults a theme can override globally, rather than being attributes on every instance.
- Reading order is source order. Collapsing never reorders children visually.
<cw-section>
A band of page content: caps and centers its content’s width, adds vertical spacing, and optionally a background.
Planned API
-
width:narrow|default|wide|full, backed by new tokens--cw-layout-width-narrow(45rem, a comfortable reading measure),--cw-layout-width-default(75rem), and--cw-layout-width-wide(90rem).fullhas no cap. padding-block: spacing token, default2x-large.gutter: spacing token for inline padding, defaultmedium.-
flush: drops the gutter when the section is narrower than--cw-section-flush-below(default40rem), so content and background run edge to edge on phones. -
label: when set, the section becomes a named region landmark (role="region"witharia-label). Without it, no landmark role — an unnamed region isn’t a landmark anyway, and a generic wrapper shouldn’t add landmark noise. - CSS part
base; CSS custom property--cw-section-background.
Built
Built in October 2026 to the API above, plus the surface attribute decided below and a second
custom property, --cw-section-flush-below, documented with the
--cw-layout-width-* tokens on the Layout tokens page. Like
<cw-grid> and <cw-stack>, it sets no container-type:
flush uses the same percentage trick as grid’s ratio splits,
clamp(0px, (100% - var(--cw-section-flush-below) + 0.1px) * 999, <gutter>) on the side
padding, so the gutter is 0 below the threshold and full at or above it. A label puts
role="region" and aria-label on the base part; without one, neither
is set.
Decided scoping questions
Both decided in October 2026, as the leans proposed:
-
Full bleed: no breakout. A background only reaches the viewport edges if nothing above
the section pads the page. Breaking out of a padded parent (
margin-inline: calc(50% - 50vw)) causes a horizontal scrollbar whenever a vertical one is present, so<cw-section>doesn’t try. Its docs say a page container should leave horizontal padding to its sections. -
Background: a
surfaceattribute. A CSS custom property reaches page builders as a free-text field, sosurface(default|alt|primary|inverse) maps to color tokens and shows up as a dropdown.--cw-section-backgroundstays for anything else.
<cw-grid>
Lays its children out in columns, one child per cell, filling rows left to right.
Planned API
-
columns: equal counts1–6, or ratio splits2-1,1-2,3-1,1-3,2-1-1,1-2-1,1-1-2. Default2. -
Equal columns treat
columnsas a maximum. Tracks arerepeat(auto-fill, minmax(max(<min-column-width>, (100% - (n - 1) * <gap>) / n), 1fr)): the grid drops to fewer columns whenever a column would be narrower thanmin-column-width, with no breakpoint to tune.auto-fill(notauto-fit) keeps a partly filled last row at the same item width. min-column-width: a CSS length, default15rem.-
Ratio splits use fixed
minmax(0, Nfr)tracks and go straight to one column below--cw-grid-stack-below(default40rem) — stepping through intermediate counts (2–1 to 1–1) reads as a different layout, not a narrower one. gap: spacing token, defaultlarge.-
align:stretch(default, so cards in a row are equal height) |start|center|end. - CSS part
base.
Built
Built in October 2026 and published in 1.6.0-alpha.3, ahead of
<cw-stack> (out of the work-plan order below), to the API above, with two implementation
decisions:
-
Ratio splits wrap with flex, not a container query. A custom property can’t appear in a
@containercondition, so a query couldn’t honor--cw-grid-stack-below. Each item’sflex-basisismax(<its share of the row>, (var(--cw-grid-stack-below) - 100%) * 999), which becomes huge below the threshold and puts every item on its own line. Equal columns stay CSS grid. Item spans (grid-column) therefore only work on equal columns. -
No
container-typeon the host. Neither layout needs a container query, since both size from percentages of the grid’s own width, so<cw-grid>avoids the containment trade-off described in the shared rules.
Open scoping questions
-
Spanning.
style="grid-column: span 2"on a child already works. A<cw-grid-item span="2">is deferred until there’s a real need — spans also fight the automatic column drop. - Page-builder wrappers. A builder that wraps each slotted child in its own element (for selection outlines, say) would make that wrapper the grid item. Check against Drupal Canvas’s editor before release, and document any workaround.
<cw-stack>
Spaces its children vertically with one consistent gap — the usual way to put a heading, text, and a button into a single grid cell.
Planned API
-
gap: spacing token, defaultmedium. Implemented as a flex column withgap, so children’s own margins are left alone. -
align:stretch(default) |start|center|end, on the cross axis. - CSS part
base.
Built
Built in October 2026 and published in 1.6.0-alpha.4, to the API above. Like
<cw-grid>, it sets no container-type: a vertical stack has nothing to change
at narrow widths, so it has no use for a container query. The check-warn-fall-back logic grid and stack both
need for enumerated attributes now lives in a shared helper (src/internal/option.ts), which
<cw-section> will use too.
Open scoping questions
-
Push to bottom. Every Layout’s “split after” (pushing trailing children to the bottom,
like a card’s button row lining up across a grid) is deferred to see whether
<cw-grid align="stretch">plus a flexible child covers it.
Layout work plan
Each step is its own issue and merge request, in this order. Every component also meets the baseline in How releases are planned and the contributor minimum: a render test, an accessibility test, and a test per advertised feature.
-
Foundations.
Done (October 2026): the spacing-token helper (
src/internal/spacing.ts) and the track-count test helper (src/internal/test/layout-helpers.ts) came with<cw-grid>; the--cw-layout-width-*tokens and their Layout tokens page came with<cw-section>. Add the--cw-layout-width-*tokens tolight.cssanddark.cssand document them on a Tokens page. Add an internal helper that maps a spacing token name to itsvar(--cw-spacing-*), with the fallback-and-warn behavior, and a test helper that asserts a computed track count at a given container width. Done when the tokens are documented and both helpers are tested. -
<cw-stack>. Done (October 2026), built after<cw-grid>and published in1.6.0-alpha.4. The simplest of the three, built first to prove the token, slot, and container-query conventions. Done when every attribute is tested and its docs page has live examples. -
<cw-grid>. Done (October 2026), published in1.6.0-alpha.3. Done when tests cover each equal column count in a wide container, the drop to fewer columns as the container narrows, each ratio split and its stack threshold,gapandalignmapping, and the fallback for an invalidcolumnsvalue; and its docs page shows a grid of cards and a grid inside a narrow container. -
<cw-section>. Built (October 2026), not yet published; both scoping questions are decided. Done when tests cover each width cap,flushat both sides of its threshold,labelproducing a named region, and no landmark role without it. - Layout guide. Done (October 2026): Page Layout, under Getting Started. A docs page composing all three into a full page example (section, grid of cards, stacks inside cards), linked from each component’s page.
-
Release.
So far:
1.6.0-alpha.2(<cw-picture>,<cw-video-embed>),1.6.0-alpha.3(<cw-grid>),1.6.0-alpha.4(<cw-stack>). Publish each component as a1.6.0alpha as it lands, then1.6.0stable with changelog entries for all five v1.6 components.
More primitives — <cw-cluster>, <cw-sidebar>,
<cw-switcher> — wait until these three have been used on real pages; see Beyond v1.9
below.
v1.7 — Media Players
The self-hosted players, moved out of v1.6 in October 2026. They share a playback engine, so they’re planned and built together, and each has a scoping question below that needs a decision before implementation starts.
<cw-audio-player>
A player for self-hosted audio, podcast episodes in particular — inspired by
Red Hat Design System’s rh-audio-player, which wraps a real <audio> element with custom controls
rather than replacing it:
-
Three sizes (
full,compact,mini) differing only in how much extra content is visible — full-size needs a wide viewport; compact/mini work at any breakpoint and can anchor to the top or bottom of a page as a persistent bar - Playback controls, seek timeline, and volume, all ARIA-labeled and keyboard-operable
- Expandable panels for supplementary content: an “about the episode” panel, a “subscribe” panel linking to podcast platforms, and a synced transcript that auto-scrolls and highlights words as they’re spoken
- Poster/artwork image slot
Open scoping question: transcript-sync (word-level highlighting tied to playback position) is the most technically demanding piece here — it needs either pre-timestamped caption data (WebVTT cues) or a forced-alignment step to generate timestamps from a plain transcript. Worth deciding up front whether v1 requires the consumer to supply timestamped data, or ships without transcript sync and adds it later.
<cw-video-player>
A real player for self-hosted video — not a third-party embed facade. Shares its core engine
(play/pause/seek/volume/keyboard handling) with <cw-audio-player>, since both wrap the
same underlying HTMLMediaElement API, but ships as a separate element rather than one component
with a mode switch — video’s visual chrome genuinely diverges from audio’s (a visible frame, fullscreen,
picture-in-picture, on-screen captions), and audio’s compact/mini page-anchored bar pattern doesn’t apply to
video:
- Native
<video>wrapping, poster image, fullscreen and picture-in-picture toggles -
Caption/subtitle track selection (
<track kind="captions">), rendered on-screen rather than in a side transcript -
Shares the about/subscribe/transcript panel components with
<cw-audio-player>where they make sense for video content too
Open scoping question — the big one: self-hosting without a platform like YouTube or Vimeo
means no adaptive bitrate streaming out of the box. <video> alone just plays one
fixed-quality file, which is a poor experience on a slow connection. Real adaptive delivery means
integrating something like hls.js or dash.js to feed an HLS/DASH manifest into Media Source Extensions — a
meaningfully bigger scope than the player UI itself, and partly a hosting-side problem (manifest generation)
outside cw-elements entirely. Needs an explicit decision before implementation starts: ship v1 as
single-quality-file playback only, or commit to adaptive streaming from the start.
v1.8 — Performance, Theming & Polish
A consolidation release: make all previously-shipped components production-ready at scale, and address longstanding theming gaps.
Virtualized rendering
Opt-in virtual scrolling for components that can hold large datasets:
<cw-select>(long option lists)<cw-tree>(large hierarchies)<cw-data-table>(the deferred item from v1.2)
<cw-file-upload>
A drag-and-drop file input with a styled dropzone, file list, and per-file progress using the existing
<cw-progress-bar>.
Theming improvements
-
Theme builder tooling/docs to make custom themes easier to author (today there’s only
light.css/dark.css/_utility.css) - Evaluate a high-contrast theme for accessibility
Accessibility & RTL audit
A focused pass across every component introduced so far: screen reader testing, keyboard navigation review, and right-to-left layout verification.
Re-enable Firefox in the test suite
web-test-runner.config.js currently only runs the default test group against
Chromium and WebKit — Firefox is commented out (it “started failing randomly,” per the note left in that
file) and there’s no Firefox coverage anywhere today. Root-cause the flakiness and bring
playwrightLauncher({ product: 'firefox' }) back into the browsers array so all
three engines run in CI. Worth prioritizing within this release rather than leaving indefinitely deferred,
since it’s the primary browser for at least some of this project’s own maintainers.
Enforce JSDoc comments via ESLint
.eslintrc.cjs has no jsdoc/require-jsdoc plugin configured today, so
nothing catches functions documented with ad hoc // banner comments instead of proper
/** ... */ blocks — found while cleaning up
src/internal/test/form-control-base-tests.ts, a holdover from the Shoelace fork. Add an ESLint
JSDoc plugin (e.g. eslint-plugin-jsdoc) with a require-jsdoc-equivalent rule for
exported/module-level functions, then sweep the codebase for existing offenders it flags.
v1.9 — Coach Marks, Notifications & Data Viz Primitives
Sourced from real-world usage rather than a wishlist: the
chromium-dashboard real-world test from v1.4 turned up
74 custom chromedash-* components in that codebase. Auditing them for genuinely reusable UI
patterns — as opposed to chromestatus-specific business pages (feature tracking, guides, admin views, intent
forms, which make up the other ~55 and don’t generalize into design-system components) — surfaced a few
things the team had to build from scratch because neither Shoelace nor Cordwainer had an equivalent. That’s
a stronger signal for what to build next than guessing.
<cw-coach-mark>
Inspired by chromedash’s chromedash-callout (a name that undersells it — it’s not a docs-style
admonition, it’s a dismissible bubble anchored to a target element for pointing out a new feature):
-
Directional anchor positioning (north/south/east/west) reusing
@floating-ui/dom— already a dependency, per<cw-popover>in v1.5 -
Persistent dismissal via a consumer-supplied
cueidentifier, so a dismissed coach mark stays dismissed (chromedash ties this to signed-in user state) rather than reappearing every page load -
Distinct from
<cw-tooltip>(hover-only, not persistent) — good fit for onboarding tours and “new feature” spotlights
<cw-timeline>
Inspired by chromedash’s chromedash-gantt (412 lines, built entirely from scratch) — this is
the “lightweight data-visualization primitives” idea previously sitting in the Beyond-v1.9 backlog below,
now with a concrete real-world shape to scope against:
- Category rows, each holding one or more date-ranged milestone bars on a shared timeline axis
- Responsive fallback to a plain bullet list of milestones on narrow viewports, rather than trying to cram a horizontal chart into a small screen
<cw-textarea> pattern validation
Inspired by chromedash’s chromedash-textarea, which subclasses
CwTextarea specifically to add regex validation — something native
<textarea> has no attribute for at all, unlike <input pattern>:
-
A
patternattribute on<cw-textarea>matching<cw-input>’s existing validation model -
Open scoping question: chromedash’s specific need goes further — a “multi-value” mode
where the textarea holds a delimited list and each item is validated against its own sub-pattern. Worth
deciding whether that belongs in core or stays a documented composite-component recipe, the way
chromedash-textareaitself is one.
Investigated, not pursued (yet)
-
Toast/snackbar. Chromedash’s
chromedash-toastis a from-scratch single-message, single-action-link toast with sequential queueing, rather than using<cw-alert>’s existingtoast()/toast-stack mechanism.<cw-alert>already supports imperative creation, a toast stack, and durations — before building a new component here, worth confirming what it actually can’t do (an action-link convention, sequential queueing behavior) versus documenting how to get chromedash’s exact pattern out of<cw-alert>as-is. -
Typeahead/autocomplete. Chromedash’s
chromedash-typeaheadsubclassesSlDropdown/SlMenuItemspecifically to strip out code that steals keyboard focus during arrow-key navigation — a real problem, but one<cw-combobox>(shipped since the 1.0 fork, after chromedash’s typeahead was built) looks purpose-built to solve correctly via proper combobox focus handling. Not a gap in Cordwainer today; more likely a sign that chromedash’s own custom typeahead predates<cw-combobox>and could be retired in favor of it. -
Meter-with-link, sortable ranked list.
chromedash-x-meter(a linked percentage bar) andchromedash-stack-rank(a click-to-sort ranked list built on it) read as compositions of existing/planned primitives —<cw-progress-bar>and<cw-data-table>(from v1.2, virtualized in v1.8) — rather than new component categories. A documented usage recipe fits better than new components.
Beyond v1.9
Ideas that are on our radar but not yet scheduled into a specific release:
- Drag-and-drop sorting utilities (for
<cw-tree>, lists, and the data table) - Additional framework integrations beyond React/Vue/Angular/Svelte (e.g., SolidJS, Qwik)
-
More layout primitives once v1.6’s
<cw-section>,<cw-grid>, and<cw-stack>have been used on real pages:<cw-cluster>(inline items that wrap with even gaps, like tags or button rows),<cw-sidebar>(a fixed-width side beside a fluid main, wrapping when narrow), and<cw-switcher>(a row that becomes a column below a threshold)
Have thoughts on sequencing, or a use case that needs something sooner? Open a work item — community input directly shapes how this roadmap evolves.