@rowan-ui/core
v0.22.0
Published
Rowan vanilla Web Components design system.
Maintainers
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/coreIcons 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/iconsUse 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.
- Primitive tokens, for example
--rowan-color-forest-700,--rowan-space-4 - Semantic tokens, for example
--rowan-color-bg,--rowan-color-fg,--rowan-color-accent - 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 isrowan-combobox,rowan-multi-select-combobox, androwan-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, androwan-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-sortevent and.sortBy(id, dir) - Pagination model with
rowan-page-changeand 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(stringor `{ id, subtotal
