npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rowan-ui/core

v0.22.0

Published

Rowan vanilla Web Components design system.

Readme

Rowan UI Core

Rowan is a plain-vanilla Web Component design system built with browser standards only: Custom Elements, Shadow DOM, slots, CSS custom properties, and ElementInternals.

Why Rowan

Rowan is named after the rowan tree (Sorbus aucuparia): small, hardy, and easy to recognize without being loud. It grows at edges and in thin soil. The scarlet berries are a detail on the tree, not the tree itself.

That is the brief for this library.

  • Vanilla first. Custom Elements, Shadow DOM, slots, CSS tokens. No framework runtime in the box.
  • Composition over inheritance. Public API is attributes, properties, slots, events, and parts. Components nest; they do not subclass each other.
  • Config where reuse matters. Tables and similar surfaces take data and cell config so the same component can be a checkbox column in one app and a link column in another.
  • Quiet theming. Primitive → semantic → component tokens. Dark mode is a token swap, not a fork.

The mark is an R with a single berry in the counter. The letter is the product. The berry is the reminder: keep the accent small.

Package name: @rowan-ui/core. Version 0.22.0. User-visible changes are in CHANGELOG.md.

0.22 surface. Public API is attributes, properties, slots, events, tokens, and CSS parts. Events fire only from user action, never because a parent set a property. Form controls are form-associated. Constraint copy is English by default; override it with @rowan-ui/core/validity-messages. Hosts that generate copy accept property-only messages; locale follows locale, inherited lang, the browser locale, then en-US. @rowan-ui/core/tokens.json is the design-token handoff; CSS stays the runtime source of truth. @rowan-ui/core/table-view-state normalizes portable table state; the app owns storage and routing. Nested filter groups and table-view-state version 2 are experimental; frozen filters stay the top-level AND list; the view-state schema is not frozen beyond version. The table toolbar slot shares one chrome with the table. rowan-filter-builder appearance="toolbar" is chips on the bar. applyQuickFind is host-owned and not a filter type. rowan-sparkline stroke is CSS pixels. Additive property-only target splits the stroke at a value. Additive variant="area" fills under the stroke. A slotted gauge sits beside KPI metrics except below 10rem. Modals use native <dialog>. Dropdown, popover, tooltip, and context menus use the top layer. Dropdown and popover re-clamp while open. Table virtualization, rowan-rich-text-editor, rowan-filter-builder, rowan-trend-chart, rowan-kpi-card, rowan-sparkline, rowan-donut-chart, rowan-bar-chart, rowan-combo-chart, rowan-scatter-chart, rowan-heatmap-chart, rowan-funnel-chart, rowan-waterfall-chart, rowan-range-chart, rowan-box-plot-chart, and rowan-radar-chart are Stable. Nested filter groups and table-view-state version 2 stay experimental. rowan-column-sparkline is Stable. This is not 1.0.

Install

npm install @rowan-ui/core

Icons are a second package. Install them next to core when you need rowan-icon-button names, slotted SVGs, or <rowan-icon>:

npm install @rowan-ui/core @rowan-ui/icons

Use Rowan

Register all Rowan elements

import "@rowan-ui/core";
import "@rowan-ui/core/tokens";
import "@rowan-ui/core/tokens/light";
import "@rowan-ui/core/tokens/dark";

Register only what you need

import "@rowan-ui/core/tokens";
import "@rowan-ui/core/button";
import "@rowan-ui/core/card";
import "@rowan-ui/core/table";

No-bundler local usage

<link rel="stylesheet" href="./src/tokens/tokens.css" />
<link rel="stylesheet" href="./src/tokens/themes/light.css" />

<script type="module">
  import "./src/button/button.js";
</script>

<rowan-button>Save</rowan-button>

TypeScript

Rowan publishes declarations with every ESM entry point. Import the registration module normally, then use the exported class or the global HTMLElementTagNameMap type:

import { RowanButton } from "@rowan-ui/core/button";
import { RowanTable, type RowanTableConfig } from "@rowan-ui/core/table";

const button: RowanButton = document.createElement("rowan-button");
const table: RowanTable = document.createElement("rowan-table");

const config: RowanTableConfig = {
  columns: [{ id: "name", header: "Name", sortable: true }],
  rows: [{ id: "1", name: "Ada" }],
};

button.disabled = true;
table.config = config;

React

The custom element is the product. Generated wrappers in @rowan-ui/core/react/<name> are the React default. They load the element module, assign objects as properties, and map onRowanClick to rowan-click. ref is the host. Import them from a client component; they are not Server Components.

import { useState } from "react";
import { RowanButton } from "@rowan-ui/core/react/button";
import { RowanTable } from "@rowan-ui/core/react/table";

const config = {
  rowId: "id",
  selectable: "multiple",
  caption: "Members",
  columns: [{ id: "name", header: "Name" }],
  rows: [{ id: "1", name: "Ada" }],
};

export function MembersTable() {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <>
      <RowanTable
        config={config}
        selected={selected}
        onRowanSelect={(event) => setSelected(event.detail.selected)}
      />
      <RowanButton onRowanClick={() => save()}>Save</RowanButton>
    </>
  );
}

Raw tags stay first-class for markup. Prefer wrappers for objects, arrays, and onRowan* events. Storybook Integrations / Using Rowan from React shows wrapper and tag side by side.

React 18 server rendering serializes a false custom-element boolean as a present attribute such as disabled="false". After the host upgrades, Rowan treats "false" as unset and removes the attribute. Generated wrappers assign booleans as properties, so disabled={false} never appears in markup.

For Next.js, import wrappers from a client component. Do not import them (or registration modules) from server-rendered code:

"use client";

import { RowanButton } from "@rowan-ui/core/react/button";
import { RowanTable } from "@rowan-ui/core/react/table";

export function RowanClientBoundary() {
  return <RowanButton onRowanClick={() => save()}>Save</RowanButton>;
}

Theming

Rowan tokens are layered so teams can theme once and keep component APIs stable.

  1. Primitive tokens, for example --rowan-color-forest-700, --rowan-space-4
  2. Semantic tokens, for example --rowan-color-bg, --rowan-color-fg, --rowan-color-accent
  3. Component tokens, for example --rowan-button-bg, --rowan-field-border

Component tokens derive from the semantic layer, so setting the semantic layer alone produces a readable theme. --rowan-color-surface is the raised surface behind cards, dialogs, and fields, and --rowan-color-accent-contrast is the text drawn on an accent fill; both default to --rowan-color-bg when a theme does not set them.

Apply a theme at the same element that declares the tokens, normally :root. A custom property that references another custom property resolves where it is declared, so a theme scoped to a nested wrapper must also re-declare the component tokens it changes. The shipped light.css, dark.css, lagoon.css, ember.css, slate.css, and midnight.css do this for you. Slate and Midnight are a zinc pair with a teal accent. They set --rowan-font-family to Inter; load that face in the host if you want it. Add another theme by copying one of those files, changing the semantic colors, and importing it. Set document.documentElement.dataset.theme to the matching name.

Tokens ship both as constructable stylesheets adopted by Rowan shadow roots and as CSS files you can import globally.

import "@rowan-ui/core/tokens";
import "@rowan-ui/core/tokens/light";
import "@rowan-ui/core/tokens/dark";
import "@rowan-ui/core/tokens/lagoon";
import "@rowan-ui/core/tokens/ember";
import "@rowan-ui/core/tokens/slate";
import "@rowan-ui/core/tokens/midnight";

document.documentElement.dataset.theme = "slate";

Design-Tool Handoff

@rowan-ui/core/tokens.json is generated from src/tokens/tokens.css alongside the constructable stylesheet. It uses DTCG-shaped $value and $type records, preserves direct CSS aliases as references such as {semantic.color.accent}, and adds $extensions["org.rowan"] metadata for the original CSS property name, CSS value, and token layer. Point a design-token importer at node_modules/@rowan-ui/core/tokens.json, or load it directly in modern ESM tooling:

import rowanTokens from "@rowan-ui/core/tokens.json" with { type: "json" };

The JSON artifact is for design-tool handoff and build-time inspection. CSS remains the runtime source of truth; no JavaScript theme provider is required. Some component values are CSS expressions, so entries with $type: "custom" should be preserved rather than coerced into a design-tool color or dimension.

Customize Semantic Tokens

Create product themes by defining the semantic layer at :root after importing Rowan's base tokens. The shipped theme validator requires these values for every first-party theme and checks readable foreground/surface and accent-contrast pairs.

:root[data-theme="workbench"] {
  --rowan-color-bg: #f4f7f2;
  --rowan-color-fg: #17231b;
  --rowan-color-muted: #4c5b50;
  --rowan-color-accent: #2b6847;
  --rowan-color-border: #c8d2c9;
  --rowan-color-danger: #b83b31;
  --rowan-color-success: #287249;
  --rowan-color-warning: #916a15;
  --rowan-color-surface: #ffffff;
  --rowan-color-accent-contrast: #ffffff;
}

For a nested theme scope, copy a shipped theme file's component aliases too. Custom properties resolve where those aliases are declared, so aliases such as --rowan-button-bg: var(--rowan-color-accent) must be re-declared inside the nested theme wrapper.

Chart and KPI tones default to those same UI colors, so forest and sand can wash out in small plots. Import @rowan-ui/core/tokens/charts-vibrant and set data-rowan-charts="vibrant" on a dashboard region (or a single chart host) for a louder blue / green / amber / red palette. Buttons, navigation, and form chrome stay on the Rowan theme.

import "@rowan-ui/core/tokens/charts-vibrant";

document.querySelector("#ops-dashboard").dataset.rowanCharts = "vibrant";

Locale-Aware Formatting

Locale formatting is a pure utility, not a display-only custom element. Import it from @rowan-ui/core/format, keep locale and time-zone policy in application state, then assign the returned string with textContent. Structured Intl options remain ordinary JavaScript objects and are never serialized to attributes.

import {
  formatCurrency,
  formatDate,
  formatNumber,
  formatRelativeTime,
} from "@rowan-ui/core/format";

const total = formatCurrency(1234.56, {
  currency: "EUR",
  locale: "de-DE",
  options: { currencyDisplay: "code" },
});
const deploymentDate = formatDate("2026-09-13T14:30:00Z", {
  locale: "en-GB",
  timeZone: "UTC",
  options: { dateStyle: "long", timeStyle: "short" },
});
const yesterday = formatRelativeTime(-1, { locale: "en-US", unit: "day" });
const unavailable = formatNumber("unknown", { fallback: "Not available" });

formatNumber, formatCurrency, formatDate, and formatRelativeTime return the caller-provided fallback for invalid input, locale, time zone, currency, or Intl options; the default fallback is an empty string. Run a formatter again whenever the application's locale or time zone changes. Table columns can use the same utility through their existing format callback:

{
  id: "total",
  header: "Total",
  type: "number",
  format: (value) => formatCurrency(value, { currency: "USD", locale: "en-US" }),
}

Component Messages, Locale Inheritance, and RTL

Components that expose a messages property accept a property-only map for their documented generated copy. Do not serialize message maps to attributes. Assigning a new map updates fallback text without emitting a user-interaction event, so the host can replace translations when application locale state changes.

Set lang and dir on an application or regional wrapper. A component resolves its locale from an explicit locale property first, then the nearest inherited lang, then the browser locale, and finally en-US. Horizontal keyboard controls follow logical inline direction under dir="rtl".

const workspace = document.querySelector("#team-workspace");
workspace.lang = "ar-EG";
workspace.dir = "rtl";

const calendar = workspace.querySelector("rowan-calendar");
calendar.messages = {
  previousMonth: "السابق",
  nextMonth: "التالي",
  dayLabel: "اختيار {date}",
};

const table = workspace.querySelector("rowan-table");
table.messages = {
  selectAllRows: "تحديد كل الصفوف",
  selectRow: "تحديد الصف {rowId}",
};

Messages only fill Rowan-generated copy. Explicit attributes, slots, configured labels, and point labels remain author-owned, even if an authored string matches a default such as Point 1. An explicit formatter remains ahead of automatic locale formatting. Consult the localization docs for the generated messages key inventory, or each component's API for callback contexts.

Constraint Messages

Rowan form controls ship English constraint strings. Locale policy stays in the application, the same way formatters do. Import @rowan-ui/core/validity-messages, replace keys or install a resolver before mounting controls, and keep setCustomValidity for per-control errors.

import {
  ROWAN_VALIDITY_MESSAGES,
  setValidityMessageResolver,
  setValidityMessages,
} from "@rowan-ui/core/validity-messages";

setValidityMessages({
  "valueMissing.checkbox": "Cochez cette case.",
});

setValidityMessageResolver((key, fallback) => {
  return translate(`rowan.${key}`, fallback);
});

ROWAN_VALIDITY_MESSAGES is the English catalog. Unknown keys resolve to an empty string. Empty resolver/override values fall through to the default. Text fields still prefer the browser's native validationMessage when the engine provides one.

Component Catalog

Implemented components currently include:

  • Actions and status: rowan-button, rowan-icon-button, rowan-link, rowan-badge, rowan-chip, rowan-avatar, rowan-alert, rowan-status-indicator, rowan-spinner, rowan-progress, rowan-skeleton, rowan-divider, rowan-empty-state, rowan-data-state, rowan-toast, rowan-toaster, rowan-file-item
  • Forms: rowan-text-field, rowan-textarea, rowan-checkbox, rowan-switch, rowan-radio, rowan-radio-group, rowan-rating, rowan-rich-text-editor, rowan-select, rowan-combobox, rowan-listbox, rowan-option, rowan-multi-select-combobox, rowan-segmented-control, rowan-date-picker, rowan-date-range-picker, rowan-time-picker, rowan-color-picker, rowan-calendar, rowan-number-field, rowan-slider, rowan-form-field, rowan-form-layout, rowan-dropzone, rowan-file-upload, rowan-validation-summary, rowan-form-wizard
  • Surfaces and overlays: rowan-card, rowan-dialog, rowan-confirm-dialog, rowan-command-palette, rowan-command-item, rowan-context-menu, rowan-drawer, rowan-dropdown, rowan-popover, rowan-tooltip
  • Navigation and workspaces: rowan-menu, rowan-menu-item, rowan-tabs, rowan-tab, rowan-tab-panel, rowan-tree, rowan-tree-item, rowan-side-nav, rowan-side-nav-item, rowan-side-nav-section, rowan-app-layout, rowan-split-pane, rowan-accordion, rowan-pagination, rowan-breadcrumb, rowan-stepper, rowan-carousel
  • Data display and operations: rowan-trend-chart, rowan-area-chart, rowan-bar-chart, rowan-stacked-area-chart, rowan-stacked-bar-chart, rowan-combo-chart, rowan-scatter-chart, rowan-heatmap-chart, rowan-funnel-chart, rowan-waterfall-chart, rowan-range-chart, rowan-box-plot-chart, rowan-radar-chart, rowan-donut-chart, rowan-sparkline, rowan-column-sparkline, rowan-bullet-chart, rowan-gauge-chart, rowan-kpi-card, rowan-source-meta, rowan-image, rowan-virtual-list, rowan-table, rowan-table-toolbar, rowan-bulk-actions-bar, rowan-filter-builder, rowan-row-details-panel

API stability

Everything in the catalog above is usable. The listed dashboard hosts are frozen; pin the version if you depend on them. This is not 1.0.

| Surface | Status | Notes | | ------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Primitives, forms, overlays | Stable | Attributes, properties, slots, events, and parts are settled. | | rowan-table config and events | Stable | columns, rows, selection, sorting, and paging are settled. | | Table virtualization | Stable | virtualized, virtualItemSize, virtualOverscan, and --rowan-table-virtual-height will not be renamed. | | rowan-rich-text-editor | Stable | Blocks are paragraph, heading (1–3), unordered-list, and ordered-list. Runs are bold, italic, underline, and allowlisted href. HTML is never a value. Images are not document nodes. | | rowan-filter-builder | Stable | Frozen top-level AND list of { id, field, operator, value }. Nested { join, filters } groups are experimental. Unknown types and operators coerce. Additive appearance: panel (default, always-open rows) or toolbar (chips + popover editor). | | rowan-trend-chart | Stable | Small multi-series line chart. series, labels, config, and valueFormatter are frozen. Other geometries are separate hosts. | | rowan-kpi-card | Stable | label, tone, delta-label, loading; property-only value and delta. Chart slot is for compact charts. Inline-size container: compact below 10rem, roomy from 22rem. Does not change chart kind. | | rowan-sparkline | Stable | One series, property-only values / labels. Null is a gap. Additive variant="area" fills under the stroke. Additive property-only target splits the stroke with above-tone / below-tone. Not a density of rowan-trend-chart. | | rowan-column-sparkline | Stable | Compact one-series columns. Property-only values / labels / target. Null is a gap. Not a density of rowan-bar-chart and not a variant of rowan-sparkline. Columns stay time-LTR in RTL. | | rowan-bullet-chart | Stable | Few bullet graph. encoding: ink (default), accent, status, or tone. ⅓-height actual, target tick. scale ticks (default on), intent higher|lower. | | rowan-gauge-chart | Stable | Speedometer gauge. min / max attributes, property-only ranges / value / target. Null is no-data. | | rowan-donut-chart | Stable | First series only. Negative values are no-data and omitted from the total. Additive variant: donut (default) or pie (no hole; total stays in the table). | | rowan-bar-chart | Stable | Small categorical bars. series / labels / config / valueFormatter. Null is no-data. Additive orientation: vertical (default) or horizontal. | | rowan-area-chart | Stable | Filled multi-series. Same property-only series / labels / config / valueFormatter as the line chart. Null breaks the fill. Independent fills, not stacked. referenceLines are frozen on this host only. | | rowan-stacked-area-chart | Stable | Positive values stack from zero as filled bands. Null and negatives are no-data. Additive stack-mode (absolute default, or normalized 0–100). Independent fills stay on rowan-area-chart. | | rowan-stacked-bar-chart | Stable | Positive values stack from zero. Null and negatives are no-data. referenceLines are frozen on this host only. Additive orientation and stack-mode (absolute default, or normalized 0–100). | | rowan-combo-chart | Stable | Bars, lines, and areas on one category axis. Per-series geometry (bar default) and optional axis: "secondary". Series overlay; they do not stack. createParetoData is a helper; the host does not auto-sort. | | rowan-scatter-chart | Stable | Points { x, y, size?, label? }. Null x or y is no-data. size encodes bubble radius. Not categorical series.values. | | rowan-heatmap-chart | Stable | Categorical matrix. rows / columns / values[][], or points. Single-hue intensity via fill-opacity. Null is empty. | | rowan-funnel-chart | Stable | First series, input order. variant: funnel (default), cone, or pyramid. Null and negatives are no-data. The host does not auto-sort. | | rowan-waterfall-chart | Stable | First series is signed deltas. { type: "total" } is an absolute bar from zero. The host does not invent totals. Null is no-data. | | rowan-range-chart | Stable | Per-category { low, high }. variant: bar (default) or area. Null low or high is no-data. Not categorical series.values. | | rowan-box-plot-chart | Stable | Per-category { min, q1, median, q3, max, outliers? }. The host does not compute quartiles. Null in the five-number summary is no-data. | | rowan-radar-chart | Stable | Categories are axes around the ring. geometry: line (default) or area. Null is no-data and breaks the ring. Nightingale and radial bars are not this host. | | rowan-image | Stable | Display still with src, alt, optional href, overlay and caption slots. Not a rich-text node. | | rowan-data-state | Stable | Region wrapper for ready / loading / empty / error only. Table and KPI keep their own loading. | | rowan-source-meta | Stable | Provenance line (source, as-of). Drop into KPI description, chart description, or table caption. No new props on those hosts. |

Rowan Data State

rowan-data-state is a frozen region wrapper for dashboard chrome. state is ready (default), loading, empty, or error. Ready content is the default slot. Named slots loading, empty, and error replace the fallback copy. actions (retry) is shown for empty and error. The wrapper does not fetch data or emit events; retry is the author's button. Table and KPI keep their own loading.

<rowan-data-state state="error">
  <p>Fill rate is 82%.</p>
  <p slot="error">The fill-rate query failed.</p>
  <button slot="actions" type="button">Retry</button>
</rowan-data-state>

Rowan Source Meta

rowan-source-meta is a frozen provenance line. source and as-of are reflected text. Named slots source and as-of replace the attributes. Drop the host into a KPI default slot, a chart description slot, or a table caption slot. Frozen KPI, chart, and table APIs do not gain source fields.

<rowan-kpi-card label="Fill rate">
  <rowan-source-meta source="Warehouse events" as-of="2026-09-23 14:02 UTC"></rowan-source-meta>
</rowan-kpi-card>

Dashboard filters

createDashboardFilters() in @rowan-ui/core/dashboard-filters is an in-memory session: set, clear, subscribe, snapshot, and replace. Apps listen to rowan-point-activate, rowan-change, and rowan-select and call set themselves. Components do not auto-subscribe. snapshot() is a shallow copy for deep links; writing the URL is the app's concern. Storybook Workflows / Dashboard filters shows the wiring.

import { createDashboardFilters } from "@rowan-ui/core/dashboard-filters";
import { applyFilters } from "@rowan-ui/core/filter-builder";

const session = createDashboardFilters();
session.subscribe((snapshot) => {
  table.rows = applyFilters(
    rows,
    Object.entries(snapshot).map(([field, value]) => ({
      field,
      operator: "equals",
      value,
    })),
  );
});

chart.addEventListener("rowan-point-activate", (event) => {
  session.set("day", event.detail.label);
});

Table view state

normalizeTableViewState(), mergeTableViewState(), serializeTableViewState(), and restoreTableViewState() in @rowan-ui/core/table-view-state are pure helpers for sort, page, filters, visible columns, density, and groupBy. They do not touch a table, URL, storage, network, or router. Assigning the restored object onto rowan-table and rowan-filter-builder is silent application control. Filters stay the frozen flat AND list unless an experimental group is present; those snapshots use version 2. visibleColumns: null preserves the source column visibility.

import {
  normalizeTableViewState,
  serializeTableViewState,
  restoreTableViewState,
} from "@rowan-ui/core/table-view-state";

const snapshot = serializeTableViewState({
  sort: { id: "opened", dir: "desc" },
  page: { index: 0, size: 25 },
  filters: [{ field: "status", operator: "equals", value: "open" }],
  density: "sm",
});

const view = restoreTableViewState(snapshot);
table.sort = view.sort;
table.page = view.page;
filterBuilder.filters = view.filters;

Rowan Carousel

rowan-carousel presents a bounded sequence of directly slotted panels. Its scalar active-index is zero-based and reflected; setting activeIndex or calling goTo(), previous(), or next() is silent application control. Only user use of the controls or Arrow, Page, Home, and End keys emits the composed rowan-change event. Rowan does not autoplay or serialize panel data.

<rowan-carousel id="release-highlights" label="Release highlights" active-index="1">
  <article>
    <h3>Release readiness</h3>
    <p>Rollback coverage is in place for the deployment window.</p>
  </article>
  <article>
    <h3>Service coverage</h3>
    <p>Confirm the weekend handoff before the window opens.</p>
  </article>
</rowan-carousel>

<script type="module">
  import "@rowan-ui/core/carousel";

  const carousel = document.querySelector("#release-highlights");
  carousel.addEventListener("rowan-change", (event) => {
    console.log(event.detail.activeIndex, event.detail.previousIndex);
  });
</script>

List Control Keyboard Model

Rowan uses one rule for every control that presents a collection, so behaviour is predictable across components:

  • Input with a popup list keeps DOM focus in the input and reports the highlighted option with aria-activedescendant. This is rowan-combobox, rowan-multi-select-combobox, and rowan-command-palette, and it follows the APG combobox pattern.
  • Standalone collection moves DOM focus between items with a roving tab index. This is rowan-listbox, rowan-menu, rowan-tabs, and rowan-tree.

rowan-select stays a thin wrapper over the native <select>, so the platform provides its popup and keyboard behaviour.

rowan-combobox supports Arrow Up and Down to open and move the highlight, Home and End to jump, Enter to commit the highlighted option, Escape to close without committing, and Tab to close. Typing filters the list. It accepts free text, so the committed value does not have to match an option.

rowan-multi-select-combobox uses the same movement keys. Enter toggles the highlighted option and keeps the popup open, Backspace on an empty query removes the last chip, and selected values stay property-only.

Optional Integrations

@rowan-ui/icons is published next to core. It provides 2,098 individually importable SVG icon modules without adding icon assets or an all-icons name registry to @rowan-ui/core. For declarative HTML, an individual @rowan-ui/icons/elements/calendar-days import registers icon="calendar-days" for rowan-icon-button and <rowan-icon name="calendar-days"> for other component slots. Icons are decorative by default and become meaningful only when callers pass an explicit accessible label. React: import { RowanIcon } from "@rowan-ui/icons/react/icon" after a per-icon @rowan-ui/icons/elements/<name> import. The wrapper optionally peers on @rowan-ui/core; SVG and HTML usage stay core-free. See the package README.

@rowan-ui/maplibre provides rowan-maplibre-map without adding a map runtime to @rowan-ui/core. Install it with maplibre-gl only in applications that need map rendering. Applications own the style or permitted tiles, provider attribution, credentials, cost and privacy decisions, and offline policy. The adapter accepts coordinates only; it does not geocode or look up addresses. Locations and marker layers are property-only, and accessible list and table alternatives remain available when map rendering cannot start. React: import { RowanMapLibreMap } from "@rowan-ui/maplibre/react/map". See the package README for the application-owned setup contract.

Rowan Selection Controls

rowan-listbox composes light-DOM rowan-option children for accessible single or multiple selection. Its scalar value reflects for declarative single-selection bindings; its selected array is property-only for multiple values. User selection emits rowan-change with the activated value, selected values, and option reference. Form-associated listboxes submit one value per selected option in multiple mode.

rowan-multi-select-combobox uses property-only options and selected arrays. It filters available choices, preserves selections outside the active query, provides removable chips, and emits one outer rowan-change event for user additions or removals. Its FACE value is a repeated form entry under name.

rowan-radio-group is form-associated. A name submits the selected child value once; required is valueMissing until a radio is selected; form reset restores the value present at connect. Child radios keep their own validity, but they do not also submit when the group has a name.

rowan-file-upload is form-associated. A name submits queued File objects; required is valueMissing until at least one file is queued; form reset clears the queue without emitting.

rowan-segmented-control is a compact, property-configured radio-group for mutually exclusive modes. Set its options array and reflected value; user click and Arrow-key changes emit rowan-change, while parent-assigned values remain silent.

<rowan-listbox id="team-list" name="team" label="Teams" selection="multiple">
  <rowan-option value="design">Design</rowan-option>
  <rowan-option value="engineering">Engineering</rowan-option>
</rowan-listbox>

<rowan-multi-select-combobox
  id="teams"
  name="assigned-team"
  label="Assigned teams"
></rowan-multi-select-combobox>
<rowan-segmented-control id="view-mode" name="view" label="View mode"></rowan-segmented-control>

<script type="module">
  import "@rowan-ui/core/listbox";
  import "@rowan-ui/core/multi-select-combobox";
  import "@rowan-ui/core/segmented-control";

  const listbox = document.querySelector("#team-list");
  listbox.selected = ["design"];

  const teams = document.querySelector("#teams");
  teams.options = [
    { value: "design", label: "Design" },
    { value: "engineering", label: "Engineering" },
  ];
  teams.selected = ["design"];

  const viewMode = document.querySelector("#view-mode");
  viewMode.options = [
    { value: "board", label: "Board" },
    { value: "list", label: "List" },
  ];
  viewMode.value = "board";
</script>

Rowan Color Picker

rowan-color-picker is a form-associated semantic color control. Its default approved palette uses Rowan's existing forest, sand, ink, and danger primitive values. The reflected value normalizes to lowercase #rrggbb for opaque colors or #rrggbbaa when opacity is below 100%.

Assign application-approved swatches with the property-only palette array. Users can also choose an arbitrary RGB color and opacity through the native fallback controls. Parent-assigned values stay silent; user selection emits rowan-change with value, color, alpha, option, and source.

<rowan-color-picker
  id="project-color"
  name="projectColor"
  label="Project color"
  value="#1d432f"
></rowan-color-picker>

<script type="module">
  import "@rowan-ui/core/color-picker";

  const picker = document.querySelector("#project-color");
  picker.palette = [
    { value: "#1d432f", label: "Forest" },
    { value: "#24543c", label: "Canopy" },
    { value: "#b4392d", label: "Signal" },
  ];

  picker.addEventListener("rowan-change", (event) => {
    console.log(event.detail.value);
  });
</script>

Rowan Rating

rowan-rating is a form-associated whole-star rating control. Its scalar value is an empty string for no rating or an integer normalized within min, max, and step; the default scale is one through five. When required is present, an unset value is invalid. User input through stars, Arrow keys, Home, End, or the clear control emits one rowan-change event; parent-assigned values remain silent.

<rowan-rating
  id="service-quality"
  name="serviceQuality"
  label="Service quality"
  value="4"
  required
></rowan-rating>

<script type="module">
  import "@rowan-ui/core/rating";

  const rating = document.querySelector("#service-quality");
  rating.addEventListener("rowan-change", (event) => {
    console.log(event.detail.value);
  });
</script>

Rowan Rich Text Editor

rowan-rich-text-editor is a form-associated authoring control for concise operational guidance. Its property-only value is a document object: paragraph, heading (levels 1–3), ordered-list, and unordered-list blocks plus bold, italic, underline, and optional allowlisted href runs. Images are not document nodes. It never accepts an HTML value or reflects document data to an attribute. Import RowanRichTextDocument from @rowan-ui/core/rich-text-editor. Link hrefs are http:, https:, mailto:, in-app paths starting with /, or fragments starting with #. Other schemes are dropped.

Rich mode uses the browser editing surface and native undo behavior. Rich clipboard data is inserted as literal plain text, not interpreted as markup. Use mode="plain" when the workflow must use a textarea fallback; it still emits the same document object. The submitted FACE value is the JSON serialization of that document.

<rowan-rich-text-editor
  id="incident-guidance"
  name="incidentGuidance"
  label="Incident guidance"
  placeholder="Describe the response steps"
></rowan-rich-text-editor>

<script type="module">
  import "@rowan-ui/core/rich-text-editor";

  const editor = document.querySelector("#incident-guidance");
  editor.value = {
    blocks: [
      { type: "heading", level: 2, children: [{ text: "Escalation" }] },
      {
        type: "paragraph",
        children: [
          { text: "Escalate to the incident lead.", bold: true },
          { text: " Open the record", href: "/incidents/12" },
        ],
      },
      {
        type: "unordered-list",
        items: [[{ text: "Open the incident record" }], [{ text: "Notify the on-call team" }]],
      },
    ],
  };

  editor.addEventListener("rowan-change", (event) => {
    persistGuidance(event.detail.value);
  });
</script>

This component's security boundary ends at its normalized document value: applications own authorization, persistence, rendering outside the component, and any collaboration or merge model. Do not turn event data into HTML; render its text runs through DOM text nodes or an application-owned trusted renderer.

Rowan Image

rowan-image is a frozen display host for a still. src and alt are attributes. Optional href is a real link around the image only, using the same navigation sanitizer as rowan-link. Overlay actions stay outside that link. A caption slot renders as figcaption. Missing or failed images show the fallback slot. It is not a rich-text document node and not a lightbox; compose a dialog if you need one.

<rowan-image src="/stills/north-yard.jpg" alt="North yard camera" href="/cameras/north-yard">
  <rowan-badge slot="overlay" tone="warning">Needs review</rowan-badge>
  <span slot="caption">North yard · live still</span>
</rowan-image>

Rowan KPI Card

rowan-kpi-card is a frozen dashboard stat tile. label, tone, and delta-label are attributes. value and delta are property-only. Tone never replaces the label. Delta text includes a sign so change is not color-only. A null value shows No data; loading replaces the value with a skeleton. The chart slot is for compact charts (rowan-sparkline, rowan-column-sparkline, rowan-bullet-chart, rowan-gauge-chart) and follows the label and value in reading order. A slotted gauge sits immediately beside the metrics except below 10rem; sparkline, column sparkline, and bullet stay under the value. The card is an inline-size container: padding, value type, and slotted chart height tighten below 10rem and open from 22rem. It does not swap chart kind or reformat value from width. Pass a formatted string when the number must go compact. Override with --rowan-kpi-card-value-size or --rowan-kpi-card-chart-height if the recipe needs a fixed scale.

<rowan-kpi-card label="Open incidents" tone="warning" delta-label="vs last week"></rowan-kpi-card>

<script type="module">
  import "@rowan-ui/core/kpi-card";

  const card = document.querySelector("rowan-kpi-card");
  card.value = 19;
  card.delta = -4;
</script>

Rowan Bar Chart

rowan-bar-chart is a frozen small categorical comparison. Property-only series, labels, config, and valueFormatter. null is no-data, not zero. Additive orientation is vertical (default) or horizontal. Omitted or unknown values stay vertical. Interactive bars emit rowan-point-activate with the same detail shape as rowan-trend-chart. Native SVG, no animation, matching data table.

Rowan Area Chart

rowan-area-chart is a frozen filled multi-series chart. It uses the same property-only series, labels, config, and valueFormatter as rowan-trend-chart. Series are independent fills, not stacked. A null value breaks both the line and the fill and appears as No data in the table. Interactive points emit rowan-point-activate. Property-only referenceLines ({ value, label?, tone? }) draw horizontal overlays. Native SVG, no animation. Stacked fills live on rowan-stacked-area-chart.

Rowan Stacked Area Chart

rowan-stacked-area-chart is a frozen categorical stack of filled bands. Positive values stack from zero in series order. null and negatives are no-data: they do not contribute height, break that series' fill, and appear as No data in the table. Other series still stack at that category. Additive stack-mode is absolute (default) or normalized (each category fills to 100; the table still shows raw values; referenceLines use the 0–100 scale). Interactive points emit rowan-point-activate. Property-only referenceLines ({ value, label?, tone? }) draw horizontal overlays. Native SVG, no animation. Independent fills stay on rowan-area-chart.

Rowan Stacked Bar Chart

rowan-stacked-bar-chart is a frozen categorical stack. Positive values stack from zero in series order. null and negatives are no-data: they do not contribute height and appear as No data in the table. Additive orientation is vertical (default) or horizontal. Additive stack-mode is absolute (default) or normalized (each category fills to 100; the table still shows raw values; referenceLines use the 0–100 scale). Interactive segments emit rowan-point-activate. Property-only referenceLines ({ value, label?, tone? }) draw overlays on the value axis. Native SVG, no animation. Grouped bars stay on rowan-bar-chart.

Rowan Combo Chart

rowan-combo-chart is a frozen categorical combo: bars, lines, and areas share one category axis. Set geometry: "bar", "line", or "area" on each series (bar is the default; unknown values fall back to bar). Areas draw behind bars, then lines. Series overlay; they do not stack. Set axis: "secondary" for a second value scale (Pareto cumulative percent). Property-only series, labels, config, and valueFormatter. null is no-data. createParetoData({ values, labels }) sorts descending and returns labels plus bar + cumulative-% series. The host does not auto-sort. Frozen bar, area, and trend charts are unchanged.

import { createParetoData } from "@rowan-ui/core/combo-chart";

const { labels, series } = createParetoData({
  labels: ["Seal", "Hinge", "Wiring"],
  values: [42, 28, 16],
});
chart.labels = labels;
chart.series = series;
chart.referenceLines = [{ value: 80, label: "80%", axis: "secondary" }];

Rowan Scatter Chart

rowan-scatter-chart is a frozen x/y point chart. Each series is { id, label, color?, points: [{ x, y, size?, label? }] }. Null x or y is no-data and is omitted from the plot and table. When any plotted point has size, radius encodes that value (bubble); omitted size is a scatter. This is not frozen categorical series.values. Interactive points emit rowan-point-activate with x, y, and size. Native SVG, no animation, matching table.

Rowan Heatmap Chart

rowan-heatmap-chart is a frozen categorical matrix. Set rows, columns, and values[][], or points as { x, y, value } / { column, row, value } triples (unique labels keep appearance order). Intensity is a single hue via fill-opacity, not a rainbow. Null is empty and No data in the table. Interactive cells emit rowan-point-activate with row, column, rowIndex, columnIndex, and value.

Rowan Funnel Chart

rowan-funnel-chart is a frozen one-series stage chart. It draws the first series. variant is funnel (default, stage 0 at the top), cone (last stage tapers to a point), or pyramid (stage 0 at the bottom). The host does not auto-sort. Null and negatives are no-data. Interactive stages emit rowan-point-activate.

Rowan Waterfall Chart

rowan-waterfall-chart is a frozen running-total chart. The first series is signed deltas. { type: "total" } is an absolute bar from zero, then sets the running total. The host does not invent totals. Null is no-data. Positive bars use success, negatives danger, totals info. Interactive bars emit rowan-point-activate with type.

Rowan Range Chart

rowan-range-chart is a frozen categorical low–high chart. Each series is { id, label, color?, values: [{ low, high }] }. variant is bar (default) or area. Null low or high is no-data. This is not frozen categorical series.values. Interactive ranges emit rowan-point-activate with low and high. Native SVG, matching table.

Rowan Box Plot Chart

rowan-box-plot-chart is a frozen five-number-summary chart. Each point is { min, q1, median, q3, max, outliers? }. The host does not compute quartiles. Null in the five-number summary is no-data. Interactive boxes emit rowan-point-activate with those fields. Native SVG, matching table.

Rowan Radar Chart

rowan-radar-chart is a frozen polar comparison. Categorical series and labels are the axes around the ring. geometry is line (default) or area. Null is no-data and breaks that series' ring; a gap is not drawn as zero. Interactive vertices emit rowan-point-activate. Native SVG, matching table. Nightingale and radial column/bar are not this host.

Histogram bins

createHistogramData({ values, bins }) from @rowan-ui/core/histogram counts samples into equal-width bins (default 5) or an explicit edge array. Nulls are omitted. Assign the result to frozen rowan-bar-chart labels / series. The bar host does not bin.

import { createHistogramData } from "@rowan-ui/core/histogram";

const { labels, series } = createHistogramData({
  values: [1, 2, 2, null, 9],
  bins: 2,
});
chart.labels = labels;
chart.series = series;

Rowan Donut Chart

rowan-donut-chart is a frozen parts-of-a-whole chart. It draws the first series. Negative values are treated as no-data: they are omitted from slices and from the hole total, and appear as No data in the table. Additive variant is donut (default, total in the hole) or pie (filled; the hole readout is hidden; the matching table still lists the slice values).

Rowan Sparkline

rowan-sparkline is a frozen compact one-series line for KPI tiles. It is a separate host, not a density of rowan-trend-chart. values is property-only (number | null; null is a gap). There is no legend, no interactive points, and no animation. Hover shows the nearest labeled value. A visually hidden table exposes the same values to assistive technology. Additive property-only target (number | null; null hides it) splits the stroke at that value: above-tone (default warning) above the target, below-tone (default success) at or below. The plot domain includes the target. A dashed target line is not a referenceLines overlay. Additive variant (line default, omitted; area fills to the plot baseline, or to target when set). Stroke width is CSS pixels (--rowan-sparkline-stroke-width, default 1.75px) and does not thicken when the plot scales. Size the plot with --rowan-sparkline-height (default 2.25rem); width is 100%. The plot fills its box (preserveAspectRatio="none"). Hover marks stay CSS pixels (--rowan-sparkline-hover-size, default 4.5px). Page-wide labeled trends belong on rowan-trend-chart. rowan-progress hide-meta hides the percent caption without dropping the progressbar name.

<rowan-kpi-card label="Open incidents" tone="warning" delta-label="vs last week">
  <rowan-sparkline slot="chart" label="Open incidents this week"></rowan-sparkline>
</rowan-kpi-card>

<script type="module">
  import "@rowan-ui/core/kpi-card";
  import "@rowan-ui/core/sparkline";

  const card = document.querySelector("rowan-kpi-card");
  const chart = document.querySelector("rowan-sparkline");
  card.value = 11;
  card.delta = -7;
  chart.values = [18, 24, 12, 15, 9, 11];
</script>

Rowan Column Sparkline

rowan-column-sparkline is a frozen compact one-series column chart for KPI tiles. It is a separate host, not a density of rowan-bar-chart and not a variant of rowan-sparkline. values is property-only (number | null; null is a gap). There is no legend, no interactive points, and no animation. Hover highlights the nearest column. A visually hidden table exposes the same values to assistive technology. Additive property-only target splits column fill with above-tone / below-tone like the line sparkline. The domain includes zero so column height encodes magnitude. Size the plot with --rowan-column-sparkline-height (default 2.25rem). Columns and nearest-column hover stay in time order (LTR) inside an RTL host. Nested theme and vibrant fills follow the nested surface.

<rowan-kpi-card label="Open incidents" tone="warning" delta-label="vs last week">
  <rowan-column-sparkline slot="chart" label="Open incidents this week"></rowan-column-sparkline>
</rowan-kpi-card>

<script type="module">
  import "@rowan-ui/core/kpi-card";
  import "@rowan-ui/core/column-sparkline";

  const card = document.querySelector("rowan-kpi-card");
  const chart = document.querySelector("rowan-column-sparkline");
  card.value = 11;
  card.delta = -7;
  chart.values = [18, 24, 12, 15, 9, 11];
</script>

Rowan Bullet Chart

rowan-bullet-chart is a frozen compact comparison after Stephen Few: qualitative ranges as grayscale intensities by default (encoding="ink"; dark = poor when intent is higher). encoding="accent" uses one theme hue at those same intensities. encoding="status" mixes danger → success (not six named greens/reds). encoding="tone" uses each range's tone. An actual value bar at one-third of the track, and a target tick. scale (default true) draws quantitative ticks. Set scale="false" in tight table cells. All of ranges / value / target are property-only. Null actual or target is no-data. Hover shows actual, target, and the matching range. The plot fills its box (preserveAspectRatio="none"). It is a separate host, not a density of rowan-bar-chart.

<rowan-kpi-card label="Fill rate" tone="success" delta-label="vs target">
  <rowan-bullet-chart slot="chart" label="Fill rate versus target"></rowan-bullet-chart>
</rowan-kpi-card>

<script type="module">
  import "@rowan-ui/core/kpi-card";
  import "@rowan-ui/core/bullet-chart";

  const card = document.querySelector("rowan-kpi-card");
  const chart = document.querySelector("rowan-bullet-chart");
  card.value = 82;
  card.delta = -8;
  chart.value = 82;
  chart.target = 90;
  chart.ranges = [
    { from: 0, to: 60, label: "Poor", tone: "danger" },
    { from: 60, to: 80, label: "Fair", tone: "warning" },
    { from: 80, to: 100, label: "Good", tone: "success" },
  ];
</script>

Rowan Gauge Chart

rowan-gauge-chart is a frozen speedometer: a semicircle scale with qualitative ranges, min / max, an actual value needle, and an optional target tick. value and target are property-only; null is no-data. min and max are attributes (default 0 and 100). If max is not greater than min, the scale falls back to 0–100. Invalid or zero-width ranges are dropped; reversed from/to are swapped. Native SVG, no animation, matching visually hidden table. Hover shows actual, target, and the matching range. Separate host from rowan-bullet-chart and rowan-donut-chart.

<rowan-kpi-card label="Utilization" tone="warning" delta-label="vs target">
  <rowan-gauge-chart
    slot="chart"
    label="Utilization versus target"
    min="0"
    max="100"
  ></rowan-gauge-chart>
</rowan-kpi-card>

<script type="module">
  import "@rowan-ui/core/kpi-card";
  import "@rowan-ui/core/gauge-chart";

  const card = document.querySelector("rowan-kpi-card");
  const chart = document.querySelector("rowan-gauge-chart");
  card.value = 72;
  card.delta = -8;
  chart.value = 72;
  chart.target = 80;
  chart.ranges = [
    { from: 0, to: 50, label: "Low", tone: "danger" },
    { from: 50, to: 80, label: "Fair", tone: "warning" },
    { from: 80, to: 100, label: "Good", tone: "success" },
  ];
</script>

Rowan Trend Chart

rowan-trend-chart is a frozen small multi-series line chart for operational comparisons such as incoming versus resolved incidents. It is scoped to small local data sets. series, labels, config, and valueFormatter are property-only APIs; no data is serialized to attributes. Import RowanTrendChartConfig from @rowan-ui/core/trend-chart. Area, bar, stacked bar, donut, sparkline, bullet, and gauge charts are separate hosts, not extra config keys.

The component always provides the same values in a semantic table beneath the chart. A null value is an intentional no-data gap: it breaks the visual line, has no interactive point control, and appears as No data in the table. Set interactive to expose each available data point as a keyboard-focusable control; Arrow keys move between points, and Enter or Space emits rowan-point-activate. It uses native SVG and no charting runtime dependency. There is no animation, so reduced-motion users receive the same stable rendering.

<rowan-trend-chart
  id="on-call-workload"
  label="On-call workload"
  description="Incoming and resolved incidents by day."
  interactive
></rowan-trend-chart>

<script type="module">
  import "@rowan-ui/core/trend-chart";

  const chart = document.querySelector("#on-call-workload");
  chart.config = {
    labels: ["Mon", "Tue", "Wed"],
    interactive: true,
    series: [
      { id: "incoming", label: "Incoming incidents", values: [18, null, 17] },
      { id: "resolved", label: "Resolved incidents", values: [13, 19, 20] },
    ],
    valueFormatter: (value, context) => (context.tick ? String(value) : `${value} incidents`),
  };

  chart.addEventListener("rowan-point-activate", (event) => {
    console.log(event.detail.seriesId, event.detail.label, event.detail.value);
  });
</script>

Rowan Command Palette

rowan-command-palette composes slotted rowan-command-item actions into a filtered modal command surface. Items accept either default slot content or a label attribute, plus optional description, keywords, group, and shortcut metadata. Disabled commands remain discoverable but are skipped by keyboard activation.

Use show(), hide(), or toggle() for parent-driven state. An optional hotkey, such as mod+k, opens the palette with Command on macOS and Control on other platforms. User activation emits rowan-command with { value, item, query }; Escape, backdrop, and close-control dismissal emit rowan-close. Parent-driven changes remain silent.

<rowan-button id="open-commands">Open commands</rowan-button>
<rowan-command-palette id="workspace-commands" label="Workspace commands" hotkey="mod+k">
  <rowan-command-item
    value="open-settings"
    label="Open settings"
    description="Update workspace preferences"
    keywords="workspace preferences account"
    shortcut="G S"
  ></rowan-command-item>
  <rowan-command-item value="invite-member" label="Invite member" shortcut="I"></rowan-command-item>
</rowan-command-palette>

<script type="module">
  import "@rowan-ui/core/button";
  import "@rowan-ui/core/command-palette";

  const palette = document.querySelector("#workspace-commands");
  document.querySelector("#open-commands").addEventListener("rowan-click", () => palette.show());

  palette.addEventListener("rowan-command", (event) => {
    console.log(event.detail.value);
  });
</script>

Rowan Contextual Actions

rowan-confirm-dialog composes the modal behavior of rowan-dialog with explicit outcomes for consequential actions. Parent changes to open remain silent. A user selection emits rowan-confirm or rowan-cancel, while Escape, the dialog close control, and backdrop dismissal emit rowan-close.

rowan-dropdown, rowan-popover, and rowan-tooltip paint on the top layer (Popover API), so overflow: hidden ancestors do not clip them. rowan-dropdown keeps a default secondary trigger and accepts a trigger slot for an avatar or icon-only control. rowan-popover trigger is click (default) or manual. Manual disables click-to-toggle; set open from app code. Escape and outside pointer still dismiss and emit rowan-change. The panel flips to the trigger end when it would overflow the viewport, and re-clamps if styles load or the panel grows while open. Hover and focus delays stay on rowan-tooltip — a popover is a dialog, not a hover surface.

Required rowan-text-field, rowan-textarea, and rowan-number-field stay visually valid until blur or reportValidity() (form submit). checkValidity() still fails immediately.

rowan-context-menu binds to an element through its for attribute or target property. It intercepts the target's native context menu, also opens from Shift+F10 or the Context Menu key, manages Arrow/Home/End menu focus, and emits rowan-change when a user selects a rowan-menu-item.

rowan-status-indicator is a compact persistent status marker. Use its tone, size, label, and optional pulse state; visible text comes from the default slot or label attribute.

<rowan-button id="archive-project" variant="danger">Archive</rowan-button>
<rowan-confirm-dialog id="archive-dialog" confirm-label="Archive" confirm-variant="danger">
  <span slot="title">Archive this project?</span>
  Archived projects remain available to workspace administrators.
</rowan-confirm-dialog>

<button id="project-target" type="button">Trail map</button>
<rowan-context-menu for="project-target" label="Trail map actions">
  <rowan-menu-item value="rename">Rename</rowan-menu-item>
  <rowan-menu-item value="archive">Archive</rowan-menu-item>
</rowan-context-menu>

<rowan-status-indicator tone="success" label="Operational"></rowan-status-indicator>

<script type="module">
  import "@rowan-ui/core/button";
  import "@rowan-ui/core/confirm-dialog";
  import "@rowan-ui/core/context-menu";
  import "@rowan-ui/core/menu-item";
  import "@rowan-ui/core/status-indicator";

  const dialog = document.querySelector("#archive-dialog");
  document.querySelector("#archive-project").addEventListener("rowan-click", () => dialog.show());
  dialog.addEventListener("rowan-confirm", () => {
    // Persist the archive action.
  });

  document.querySelector("rowan-context-menu").addEventListener("rowan-change", (event) => {
    console.log(event.detail.value);
  });
</script>

Rowan Tree

rowan-tree composes light-DOM rowan-tree-item nodes. Nest child items through the parent item’s children slot. The tree supplies roving focus and Arrow, Home, and End keyboard navigation; use ArrowRight and ArrowLeft to expand, collapse, or move into and out of a branch.

selection accepts none, single, or multiple and defaults to single. The selected values are property-only, so parent-driven changes remain silent while user changes emit rowan-change; user disclosure changes emit rowan-toggle.

<rowan-tree id="docs-tree" label="Documentation">
  <rowan-tree-item value="guides" expanded>
    Guides
    <rowan-tree-item slot="children" value="getting-started"> Getting started </rowan-tree-item>
  </rowan-tree-item>
  <rowan-tree-item value="reference">Reference</rowan-tree-item>
</rowan-tree>

<script type="module">
  import "@rowan-ui/core/tree";

  const tree = document.querySelector("#docs-tree");
  tree.selected = ["getting-started"];

  tree.addEventListener("rowan-change", (event) => {
    console.log(event.detail.selected);
  });
</script>

Rowan Application Workspaces

rowan-app-layout composes a responsive header, primary navigation, and default main-content slot. It keeps navigation state reflected through navigation-open; only user use of the compact toggle, backdrop, or Escape emits rowan-change. Compose rowan-side-nav-item children in rowan-side-nav for application destinations (Arrow, Home, End, Enter, Space). Wrap items in rowan-side-nav-section for labeled groups; the parent nav still has one value. Set collapsible for a disclosure control; collapsed defaults to open. User expand/collapse emits rowan-toggle on the section (not rowan-change on the nav). Arrow Left collapses the containing group; setting value to a child opens ancestor groups. An empty value means nothing is selected. Items accept tone and optional count for exception cues; a slotted suffix replaces the built-in status.

For a client-routed app, keep href on items and preventDefault() on rowan-change, then navigate(detail.value). Omitting href and routing only from rowan-change is also valid. Drive rowan-command-palette from the same destination list. Storybook Workflows / App shell is the copyable recipe. Do not put claims on nav items; filter before render.

rowan-split-pane provides a keyboard-operable separator for durable two-pane workspaces. Its reflected position, min, max, and step use percentage values; snapPoints is property-only. User drag or keyboard resizing emits rowan-resize, while parent assignments remain silent.

<rowan-app-layout id="workspace-shell">
  <header slot="header">Northstar</header>
  <rowan-side-nav slot="navigation" label="Workspace navigation" value="/overview">
    <rowan-side-nav-section label="Workspace">
      <rowan-side-nav-item value="/overview" href="/overview">Overview</rowan-side-nav-item>
      <rowan-side-nav-item value="/members" href="/members">Members</rowan-side-nav-item>
    </rowan-side-nav-section>
  </rowan-side-nav>
  <rowan-split-pane position="32" min="20" max="80">
    <aside slot="start">Filters</aside>
    <main slot="end">Workspace content</main>
  </rowan-split-pane>
</rowan-app-layout>

<script type="module">
  import "@rowan-ui/core/app-layout";
  import "@rowan-ui/core/side-nav";
  import "@rowan-ui/core/side-nav-section";
  import "@rowan-ui/core/split-pane";

  const nav = document.querySelector("rowan-side-nav");
  nav.addEventListener("rowan-change", (event) => {
    event.preventDefault();
    history.pushState({}, "", event.detail.value);
  });

  const pane = document.querySelector("rowan-split-pane");
  pane.snapPoints = [25, 50, 75];

  pane.addEventListener("rowan-resize", (event) => {
    console.log(event.detail.value);
  });
</script>

Rowan Virtual List

rowan-virtual-list renders a bounded, keyed window from property-only item data. Set items, itemKey, and renderItem in application code; item-size is the initial estimate, while visible items are measured with ResizeObserver. Use scrollToIndex(index, { align }) to reveal an item programmatically.

<rowan-virtual-list id="members" item-size="44" overscan="4"></rowan-virtual-list>

<script type="module">
  import "@rowan-ui/core/virtual-list";

  const members = document.querySelector("#members");
  members.items = Array.from({ length: 500 }, (_value, index) => ({
    id: `member-${index + 1}`,
    name: `Member ${index + 1}`,
  }));
  members.itemKey = "id";
  members.renderItem = (member) => {
    const row = document.createElement("div");
    row.textContent = member.name;
    return row;
  };
</script>

Rowan Table

rowan-table supports:

  • Config mode through .config, .columns, and .rows
  • Selection: none | single | multiple
  • Sorting: rowan-sort event and .sortBy(id, dir)
  • Pagination model with rowan-page-change and normalized page indexes
  • Optional virtualized body mode that retains real table markup and existing selection, sort, activation, and cell-event contracts
  • Column sticky: "start" | "end" to pin leading or trailing columns while the table scrolls horizontally. The selection column is pinned start when the table is selectable. Column DOM order does not change.
  • groupBy (string or `{ id, subtotal