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

RouteHow
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

FileWhat it is
dist/soma.cssEvery component, all three themes, all density modes. Include once.
dist/soma.jsESM bundle for bundlers/native modules.
dist/soma.umd.cjsUMD bundle: a plain <script> tag that defines window.Soma.
dist/soma.d.tsTypeScript 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

MechanismHow it works
Static classesMost components are pure CSS: render soma-* classes server-side and you're done.
Auto-initOn DOMContentLoaded, Soma binds every declarative hook it finds. The full selector list is below.
Imperative APISoma.dialog2('#id').show() and friends. Singleton per element, so calling it twice returns the same instance — safe to call after your own DOM updates.
EventsComponents 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:

SelectorComponent
.soma-dropdown2-trigger[aria-controls]Dropdown menus
.soma-messageMessages (wires the close button)
.soma-bannerBanners (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-sortableSortable tables
.soma-quicksearchQuicksearch
.soma-treeTree
.soma-expander-trigger[aria-controls]Expander
.soma-date-picker-inputDate 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-dropzoneDropzone (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-errors panel 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.html starter 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.