Plain HTML & CSS
dssoca is a Svelte 5 library, but its look and most of its behaviour are available to any HTML page — a static site, a quick demo, a CodePen — through two extra package entries:
| Entry | What it is |
|---|---|
dssoca/vanilla.css | Every component’s styles, generated at build time from the components’ own <style> blocks (never hand-written, so it cannot drift). Load it after theme.css. |
dssoca/vanilla.js | A small, dependency-free ES module that wires the interactive components over the same markup, plus a toast API and icon hydration. Optional. |
The markup contract is the exact DOM the Svelte components render. Every component page on this site has an HTML section with that markup, produced by server-rendering the real component during the docs build — copy it as-is.
1. Load the files
From npm (pnpm add dssoca) or straight from a CDN, in this order:
<html data-theme="dark" data-size-variant="md">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dssoca@0.19/dist/theme.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dssoca@0.19/dist/vanilla.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/dssoca@0.19/dist/vanilla/index.js"></script>
</head>
<body>
<button class="ss-btn primary" type="button">deploy</button>
</body>
</html> - Order matters.
vanilla.csscontains only component rules and relies on the tokens fromtheme.css(ortokens.cssif you own your global styles). It must come after them. - The module is optional. Static components (Button, Badge, Card, …) need only the CSS.
- With a bundler:
import 'dssoca/theme.css',import 'dssoca/vanilla.css', andimport { toast, mount } from 'dssoca/vanilla.js'(importing the module wires the behaviours).
Browser support
vanilla.css isolates every component with a CSS @scope block:
@scope (.ss-card) to (.ss-btn, .ss-badge, /* …every other component root… */) {
:where(:scope).ss-card { … } /* the root itself (zero-specificity anchor) */
:where(:scope).ss-card .head { … } /* only Card's own .head, never a nested Button's */
} That is what lets the components keep their short internal class names (.head, .label, .item) in a global stylesheet without leaking into each other. It is also the hard floor: @scope needs Chrome/Edge 118+, Safari 17.4+, or Firefox from early 2026. Older browsers
drop the scoped rules entirely — there is no fallback. The token layer and theme.css work
everywhere.
The two axes
Exactly as in Svelte: put data-theme="dark|light" and data-size-variant="sm|md|lg" on any
ancestor (usually <html>), or on a single component root to override it locally. From JS, applyDesignConfig is re-exported by the module:
import { applyDesignConfig } from 'dssoca/vanilla.js'
applyDesignConfig({ theme: 'light', sizeVariant: 'sm' }) Custom palettes work the same way: paste the CSS block from the Theme Builder after theme.css.
2. Interactive components
Importing vanilla.js installs one set of delegated listeners on document — nothing is
scanned or observed, so markup you inject later just works. Behaviour is keyed on the classes and
ARIA attributes the components already render; only hooks that have no natural home in the markup
use a data-ss-* attribute.
| Component | Markup hook | What the module does |
|---|---|---|
| Modal | <button data-ss-modal="#id"> opener, data-ss-dismiss inside | Native <dialog>: showModal() / close(), backdrop click closes. data-ss-static keeps it open, data-ss-no-esc blocks Escape. |
| Accordion | .head[aria-controls] (as rendered) | Toggles the panel, single-open by default (data-ss-multiple on the root allows several), Arrow/Home/End. Emits ss:change. |
| Menu | .trigger + .panel[role=menu] (as rendered) | Open/close, roving focus, Escape, outside click, focus return; radio items move the check. Emits ss:select. |
| Tooltip | .ss-tooltip[data-placement] (as rendered) | Shows on hover/focus, hides on leave/blur/Escape. Placement is used as-is (no collision flipping). |
| Switch | .track[role=switch] (as rendered) | Toggles aria-checked / .on from the track or the label. Emits ss:change. |
| SegmentedControl | .segment[role=radio], optional data-value | Click / arrows / Home / End select; emits ss:change with the data-value. |
| TierList | .tile[data-item] inside [data-zone] rows (as rendered) | Pointer drag between/within rows + keyboard sorting (Space, arrows, Space, Escape) with live announcements. data-ss-readonly on the root makes it inert. Emits ss:change with { placements }. |
| Input | .clear button (as rendered when clearable) | Empties, refocuses, hides itself until you type. |
| NumberField | .step.dec / .step.inc (as rendered) | Nudges by step, clamps to min/max, disables at the bounds. |
| Textarea | .field.autosize (as rendered) | CSS field-sizing: content where supported; scrollHeight fallback otherwise. |
| Spinner | empty .frame + data-variant="pipe" on the root | CSS only: steps() keyframes generated from the same frame tables; honours reduced motion. |
| Toaster | optional .ss-toaster[data-position] container | toast.success(…) etc. render into it (or into one the module creates). |
| Icon | <span data-ss-icon="check" data-size="sm"> | Replaced by the exact <svg class="ss-icon"> the component renders (mount() for late markup). |
Custom events bubble, so a single listener on a container works:
document.addEventListener('ss:change', (e) => console.log(e.target, e.detail)) Toasts
import { toast, toasts } from 'dssoca/vanilla.js'
toast.success('saved')
toast.error('failed', { action: { label: 'retry', onClick: () => retry() } }) // sticky
const id = toast.loading('uploading…')
toasts.update(id, { kind: 'success', message: 'done' })
toast.promise(fetch('/api'), { loading: 'saving…', success: 'saved', error: 'failed' })
toasts.max = 5 // visible cap; extra toasts queue Same names and defaults as the Svelte toast API. Swipe-to-dismiss is not ported.
Icons and late markup
import { mount, iconSvg, registerIcon } from 'dssoca/vanilla.js'
mount(container) // hydrate [data-ss-icon] + init autosize textareas you just inserted
iconSvg('arrow', { size: 'sm' }) // the SVG markup as a string
registerIcon('heart', '<path d="…"/>') // shared with the Svelte Icon registry Placeholders support data-size (xs|sm|md|lg), data-px, data-variant="solid", data-spin, data-rotate, data-flip and data-title. For a no-JS page, paste the rendered <svg> from the Icon page instead.
3. Static components
Everything else is CSS only — the markup renders correctly with vanilla.css alone, and any
behaviour is yours to add: Button, Badge, Card, Link, Avatar, Kbd, Heading, Container, MetricTile,
EmptyState, ServiceCard, Sparkline, DateField, Select, Pagination, Table (sorting), BottomNav,
FileDrop, Image (lightbox), LogStream, Sidebar, Topbar, SearchPalette, ShortcutsHelp and the
charts (Chart, ScatterPlot, BoxPlot, BumpChart, Heatmap — copy their rendered SVG).
One CSS-only behaviour worth knowing: a .ss-kbd key cap hides itself on devices without a
keyboard (@media (hover: none) and (pointer: coarse) — phones and tablets), exactly like the
Svelte component’s hideOnMobile default. Where the chip is the content rather than a hint, add data-hide-on-mobile="false" to the outer <kbd class="ss-kbd">; and hide the words around a
hint yourself, so “Enter to send” doesn’t degrade to “to send”.
See it composed into whole sites
Inspirations is a gallery of twelve example websites — an ops dashboard, a
social feed, a dating app, a team chat, a landing page, a blog, a media tracker, a storefront, a
mail client, a kanban board, a music player and an account-settings screen — each written by
hand in plain HTML on exactly this path. Open one, view its source, copy what you like. The
sites are served from the standalone Inspirations site on GitHub Pages, live in the repo under inspirations/, are rebuilt from the current source on every deploy, and their theme/size
toggles use applyDesignConfig-equivalent attributes on <html>, so they double as a test of
the two axes at page scale.
Caveats
- Internal class names are reserved inside a component. Under
@scope, any.label,.head,.item,.title… element inside a component root is styled as that component’s part — unless it sits under a nested component root. Wrap your own content or use different class names. - IDs. The Svelte components generate their
id/for/aria-controlswiring. In plain HTML you author those yourself (the snippets show working values). - Not ported: Tooltip collision avoidance, Toaster swipe-to-dismiss, and the JS of the components listed as static.
- A live HTML playground on this site is a planned follow-up.