Getting started
Summary
Soma ships as a flatpack: three built files you
can serve as static resources, with no bundler required on the
consumer side. That's the delivery model
nware-portal uses: server-rendered markup with
soma-* classes, plus one CSS and one JS tag.
Install
| Route | How |
|---|---|
| npm | npm install @nware/soma, then import
@nware/soma (ESM) and @nware/soma/css,
or @nware/soma/scss to compile the Sass yourself. |
| Flatpack (portal / plugins) | Copy dist/soma.css and dist/soma.umd.cjs
into your static resources and link them. No build step. |
| CDN (this site) | soma.nware.io doubles
as a CDN for the flatpack:
/cdn/soma.css, /cdn/soma.umd.cjs
track the latest release; /cdn/<version>/…
is version-pinned and cached immutably; /cdn/nware-soma.zip
is a downloadable archive (dist + LICENSE + NOTICE) for
vendoring. |
Release history: CHANGELOG.md.
The three files
| File | What it is |
|---|---|
dist/soma.css | Every component, all three themes, all density modes. Include once. |
dist/soma.js | ESM bundle for bundlers/native modules. |
dist/soma.umd.cjs | UMD bundle: a plain <script> tag that defines window.Soma. |
dist/soma.d.ts | TypeScript definitions for the full Soma.* API. Editors use them for autocomplete and checking even in plain-JS projects. |
Your first page
A complete page you can save and open: an info message, a primary button, and a dialog wired up with three lines of JavaScript. This is the whole integration contract:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My first Soma page</title>
<link rel="stylesheet" href="https://soma.nware.io/cdn/soma.css">
<script src="https://soma.nware.io/cdn/soma.umd.cjs"></script>
</head>
<body>
<!-- Server-rendered markup: auto-init wires it on DOMContentLoaded -->
<div class="soma-message">
<span class="soma-icon soma-icon-info"></span>
<div class="soma-message-content">
<p><strong>Welcome</strong> — this page is styled by
<a href="https://soma.nware.io" target="_blank" rel="noopener">Soma</a>.
No build step, no dependencies.</p>
</div>
</div>
<div style="text-align: center; margin-top: 24px">
<button class="soma-button soma-button-primary" id="say-hello">Say hello</button>
</div>
<dialog class="soma-dialog2 soma-dialog2-small" id="hello">
<header class="soma-dialog2-header">
<h2 class="soma-dialog2-header-main">Hello</h2>
<button class="soma-dialog2-header-close" aria-label="Close">
<span class="soma-icon soma-icon-close"></span>
</button>
</header>
<div class="soma-dialog2-content">
<p>Your first Soma dialog.</p>
</div>
<footer class="soma-dialog2-footer">
<div class="soma-dialog2-footer-actions">
<button class="soma-button soma-button-primary" id="hello-ok">OK</button>
</div>
</footer>
</dialog>
<script>
const dialog = Soma.dialog2('#hello');
document.getElementById('say-hello').addEventListener('click', () => dialog.show());
document.getElementById('hello-ok').addEventListener('click', () => dialog.hide());
</script>
</body>
</html>
The dialog is a native <dialog>, hidden by
the platform until show() opens it in the top
layer; Esc and backdrop click close it, the header close button
is wired automatically, and its accessible name is derived from
the header heading, so no explicit aria-labelledby
is needed. Pin a release by
putting the version in the path
(/cdn/0.1.0/soma.css). See CDN.
A runnable version lives at examples/flatpack.html in
the repo. It consumes dist/ alone and is covered by
the e2e suite, so this path can't silently break.
The component model
| Mechanism | How it works |
|---|---|
| Static classes | Most components are pure CSS: render soma-* classes server-side and you're done. |
| Auto-init | On DOMContentLoaded, Soma binds every declarative hook it finds. The full selector list is below. |
| Imperative API | Soma.dialog2('#id').show() and friends. Singleton per element, so calling it twice returns the same instance — safe to call after your own DOM updates. |
| Events | Components dispatch bubbling CustomEvents (soma-dialog-show, soma-tabs-change, …) so server-rendered pages can listen without holding instances. |
What auto-init binds
For most components you write markup and never touch JavaScript:
the script scans the page once on DOMContentLoaded
and binds behaviour to these selectors:
| Selector | Component |
|---|---|
.soma-dropdown2-trigger[aria-controls] | Dropdown menus |
.soma-message | Messages (wires the close button) |
.soma-banner | Banners (wires the close button, when one is present) |
.soma-tabs:not(.soma-tabs-disabled) | Tabs (-disabled opts markup out) |
.soma-inline-dialog-trigger[aria-controls] | Inline dialogs |
table.soma-table-sortable | Sortable tables |
.soma-quicksearch | Quicksearch |
.soma-tree | Tree |
.soma-expander-trigger[aria-controls] | Expander |
.soma-date-picker-input | Date picker |
[data-soma-tip] | JS tooltips (the attribute value is the placement) |
.soma-navbar[data-soma-responsive] | Responsive navbar (overflow → More menu) |
.soma-breadcrumbs[data-soma-collapse] | Breadcrumbs (middle-collapse into a More menu) |
.soma-dropzone | Dropzone (when it contains a file input) |
[data-soma-sidebar-toggle] | Shell sidebar toggle |
.soma-sidebar-group-toggle[aria-controls] | Shell sidebar nav groups (collapsible subtrees) |
Everything else is created through the Soma.* API
only: dialogs (Soma.dialog2(el).show()) and the
confirm/alert shorthands (Soma.confirm(),
Soma.alert()), toasts
(Soma.toast()), drawers (Soma.drawer()),
the command palette (Soma.palette()),
the select control (Soma.select2()), the editable
combobox (Soma.combobox()), the RESTful
table (Soma.restfulTable()), spinners
(Soma.spinner()), the progress driver
(Soma.progress()), dynamic messages
(Soma.message.create()), keyboard shortcuts
(Soma.shortcuts()), date-range pairing
(Soma.dateRange()), and sidebar resizing
(Soma.sidebarResize()).
Markup rendered after load
Auto-init runs once. If you inject markup later (AJAX, an
htmx/Turbo swap, a plugin fragment), bind everything inside the
new content with one call: Soma.scan() runs every
component's auto-init scoped to the subtree, and it is
idempotent, so already-bound elements are skipped:
container.innerHTML = renderedFragment;
Soma.scan(container);
// or bind one element explicitly — also idempotent:
Soma.tabs(container.querySelector('.soma-tabs'));
The full fragment lifecycle (scan, .destroy(),
framework hooks, CSP, SSR) is on
Integration.
Themes, density, direction
All three are attributes on <html>, with no
rebuild and no separate stylesheet. See Themes
and Density.
<html data-soma-theme="dark" data-soma-density="compact" dir="ltr">
TypeScript and editor support
The flatpack ships soma.d.ts covering the whole
Soma.* API (instance methods, option shapes, and
event names) plus the window.Soma global. In a
TypeScript project, point at it once:
// e.g. in a global.d.ts
/// <reference path="./vendor/soma/soma.d.ts" />
Plain JavaScript projects get the same autocomplete in VS Code
and IntelliJ: keep the file next to the vendored dist files
(editors pick it up via the types reference), or enable
checkJs and import types through JSDoc. Two more
editor helpers ship in the npm package and the CDN zip:
web-types.json gives IntelliJ-family IDEs
completion for every soma-* class with a link to
the right docs page (auto-detected from
node_modules), and
editor/soma.code-snippets adds VS Code skeletons
for the common markup contracts (dialog, dropdown, field,
message, banner, tabs, sortable table, toast, confirm).
Debug mode
Most integration bugs are markup-contract slips the library
can detect: a trigger whose aria-controls points
nowhere, tooltip text in the placement attribute, a tabs link
without a pane, a dialog that isn't a native
<dialog>. Debug mode makes Soma warn about
them (plain console.warn, with the fix in the
message) instead of silently doing nothing:
<!-- declaratively — scans automatically after auto-init -->
<html data-soma-debug>
// or imperatively
Soma.debug(true); // enable + scan the page now
Soma.debug.scan(el); // re-check content you injected later
Soma.debug(); // read the current state
Checks cost nothing when disabled; leave the attribute off in production.
Browser support
Evergreen browsers: the last two versions of Chrome, Firefox, Safari, and Edge. No Internet Explorer support. The JS is zero-dependency vanilla — no jQuery, no framework, no polyfills to load.
For coding agents
llms.txt is the machine-readable
contract: every class, markup shape, API and event in one file.
Point your agent at it rather than at these pages — the
reference page is just that file
rendered for humans.
Common patterns
Complete end-to-end patterns — confirm a destructive action, load a form into a dialog, map server validation errors onto fields — live on the Recipes page, where each runs live with exactly the code shown (and is covered by the test suite). Beyond those, task-oriented wiring examples live on the component pages, the ones implementers reach for first:
- Confirm before a destructive action:
Dialogs (the
Soma.confirm()shorthand and its danger appearance). - Toast on save, with close modes and the visible-stack cap: Toasts.
- Server-validated form with an error summary:
Forms (the
.soma-form-errorspanel and the accessible wiring contract). - Master–detail with a non-modal slide-over: Drawer (the collection stays clickable while the detail is open).
- Inline-editable data bound to a REST endpoint: RESTful table (incl. the endpoint contract and optimistic-removal snippet).
- Filter chips driving a table: Chips (with URL-persisted filter state).
- A complete page skeleton to copy:
Page, and the runnable
examples/app-shell.htmlstarter in the repository.
Next steps
- Integration — Soma in a living app: fragment swaps, frameworks, CSP, SSR.
- Design tokens — the
--soma-*custom properties everything is built from. - Themes — light, dark and high-contrast, all attribute-switched.
- Density — the three spacing modes: comfortable, cosy, compact.
- Component overview — every component with live examples, markup contracts, and JS APIs.
- CDN — latest vs pinned paths and the downloadable archive for vendoring.
llms.txt— the whole API contract in one machine-readable file.