Main content goes here.
A drop-in design system - no framework, no build step. Add one tag and every class and
<mosni-*> element below just works:
<script src="https://mosni.dev/mosnicat.js"></script>
<div class="alert success">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
<span>Changes saved.</span>
</div>New Active Failed
<p> <span class="badge primary">New</span> <span class="badge success">Active</span> <span class="badge error">Failed</span> </p>
<button class="btn">Continue</button> <button class="btn-ghost">Ghost</button> <button class="btn-icon" aria-label="Settings"> <mosni-icon name="menu"></mosni-icon> </button> <button class="btn btn-sm">Small</button> <button class="btn-danger">Delete</button>
<button class="btn btn-block">Continue</button>
Some text very interesting.
<div class="content-container"> <h2>Headline</h2> <p>Some text very interesting.</p> </div>
Above the divider.
Below the divider.
<p>Above the divider.</p> <hr class="divider" /> <p>Below the divider.</p>
import { Icon } from "@mosni/react";
// The glyph itself paints lazily, client-side, via the same lazy mosnicat-icons.js chunk
// <mosni-icon> uses (agent-docs → planning-artifacts/react-path-implementation-waves.md §4/§10) - under this page's static render there is no browser
// to run that effect in, so the demo below shows the (accurate) unpainted state: an empty span per
// icon. The class path / component path examples above show the painted glyphs for real.
export default function Example() {
return (
<div className="icon-grid">
<figure>
<Icon name="rocket" size={24} />
<figcaption>rocket</figcaption>
</figure>
<figure>
<Icon name="heart" size={24} />
<figcaption>heart</figcaption>
</figure>
<figure>
<Icon name="star" size={24} />
<figcaption>star</figcaption>
</figure>
</div>
);
}Pick any Lucide icon by name:
<p>
Pick any
<a href="https://lucide.dev/icons/" target="_blank">Lucide icon</a> by name:
</p>
<div class="icon-grid">
<figure>
<mosni-icon name="rocket" size="24"></mosni-icon>
<figcaption>rocket</figcaption>
</figure>
<figure>
<mosni-icon name="heart" size="24"></mosni-icon>
<figcaption>heart</figcaption>
</figure>
<figure>
<mosni-icon name="star" size="24"></mosni-icon>
<figcaption>star</figcaption>
</figure>
</div>A base link uses the house link colour and underlines on hover/focus.
<p> A base <a href="https://mosni.dev">link</a> uses the house link colour and underlines on hover/focus. </p>
Either use these as components,
Examples include tabs for both versions when available.
The shared visual chrome for every app on the Hannah's stack.
Add one script tag to the page's head — see intro.
import { Accordion, AccordionItem } from "@mosni/react";
export default function Example() {
return (
<Accordion exclusive>
<AccordionItem summary="What is Mosni-Chrome?" defaultOpen>
<p>The shared visual chrome for every app on the Hannah's stack.</p>
</AccordionItem>
<AccordionItem summary="How do I include it?">
<p>Add one script tag to the page's head — see intro.</p>
</AccordionItem>
</Accordion>
);
}The shared visual chrome for every app on the Hannah's stack.
Add one script tag to the page's head — see intro.
<mosni-accordion exclusive>
<details open>
<summary>What is Mosni-Chrome?</summary>
<p>The shared visual chrome for every app on the Hannah's stack.</p>
</details>
<details>
<summary>How do I include it?</summary>
<p>Add one script tag to the page's head — see intro.</p>
</details>
</mosni-accordion>| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
exclusive |
boolean | no | false |
Only one section open at a time - sets a shared generated name on every child <details>. |
| Name | Description |
|---|---|
toggle |
Native toggle, bubbling from each child <details>. |
import { Chips } from "@mosni/react";
export default function Example() {
return (
<Chips
label="Roles"
placeholder="Filter roles…"
filterThreshold={4}
defaultValue={["files:read", "stream:key"]}
options={[
{ value: "files:read", label: "files:read" },
{ value: "files:write", label: "files:write" },
{ value: "stream:key", label: "stream:key" },
{ value: "hub:lights", label: "hub:lights" },
{ value: "hub:heating", label: "hub:heating" },
{ value: "photos:upload", label: "photos:upload" },
]}
/>
);
}<mosni-chips label="Roles" placeholder="Filter roles…" filter-threshold="4"> <label><input type="checkbox" value="files:read" checked /> files:read</label> <label><input type="checkbox" value="files:write" /> files:write</label> <label><input type="checkbox" value="stream:key" checked /> stream:key</label> <label><input type="checkbox" value="hub:lights" /> hub:lights</label> <label><input type="checkbox" value="hub:heating" /> hub:heating</label> <label><input type="checkbox" value="photos:upload" /> photos:upload</label> </mosni-chips>
| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
label |
string | no | — |
Optional caption above the selection. |
placeholder |
string | no | Filter… |
Placeholder for the filter box. |
filter-threshold |
number | no | 8 |
Show the filter box only once there are at least this many options. |
max-height |
string | no | 13rem |
Max height of the scrolling option list, so the panel cannot grow without bound. |
empty-text |
string | no | None selected |
Shown in place of chips when nothing is selected. |
| Name | Description |
|---|---|
change |
Native change from the authored checkboxes; also dispatched when a chip's remove button clears one, so consumer listeners fire either way. |
export const define = (tag: string, cls: CustomElementConstructor): void => {
if (!customElements.get(tag)) customElements.define(tag, cls);
};import { Code } from "@mosni/react";
export default function Example() {
return (
<Code language="ts">
{`export const define = (tag: string, cls: CustomElementConstructor): void => {
if (!customElements.get(tag)) customElements.define(tag, cls);
};`}
</Code>
);
}
export const define = (tag: string, cls: CustomElementConstructor): void => {
if (!customElements.get(tag)) customElements.define(tag, cls);
};
<mosni-code language="ts">
<pre>
export const define = (tag: string, cls: CustomElementConstructor): void => {
if (!customElements.get(tag)) customElements.define(tag, cls);
};</pre>
</mosni-code>| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
language |
string | no | — |
Prism language id (ts, js, html, css, bash, json, md). |
label |
string | no | language |
Header label (defaults to language when the header shows). |
no-copy |
boolean | no | false |
Hide the copy button (shown by default). |
no-header |
boolean | no | false |
Hide the header row entirely. |
Click the trigger to open, use ↑/↓ to move between items, Esc or an outside click to dismiss. Selecting an item calls onSelect(value).
With iconOnly set, the trigger renders just the named glyph (default more-vertical) with no visible text or chevron. label still supplies the trigger's accessible name via aria-label — useful for a compact per-row action menu.
import { Dropdown, DropdownItem } from "@mosni/react";
export default function Example() {
return (
<>
<Dropdown label="Actions" onSelect={(value) => console.log(value)}>
<DropdownItem value="rename">Rename</DropdownItem>
<DropdownItem value="duplicate">Duplicate</DropdownItem>
<DropdownItem value="delete" variant="danger">
Delete
</DropdownItem>
</Dropdown>
<p>
Click the trigger to open, use <kbd>↑</kbd>/<kbd>↓</kbd> to move between
items, <kbd>Esc</kbd> or an outside click to dismiss. Selecting an item
calls <code>onSelect(value)</code>.
</p>
<Dropdown label="Actions for report.pdf" iconOnly="more-vertical">
<DropdownItem value="rename">Rename</DropdownItem>
<DropdownItem value="duplicate">Duplicate</DropdownItem>
<DropdownItem value="delete" variant="danger">
Delete
</DropdownItem>
</Dropdown>
<p>
With <code>iconOnly</code> set, the trigger renders just the named glyph
(default <code>more-vertical</code>) with no visible text or chevron.{" "}
<code>label</code> still supplies the trigger's accessible name via{" "}
<code>aria-label</code> — useful for a compact per-row action menu.
</p>
</>
);
}
Click the trigger to open, use ↑/↓ to move between
items, Esc or an outside click to dismiss. Selecting an item fires
mosni-dropdown-select with detail: { value }.
With the icon-only attribute, the trigger renders just the named
glyph (default more-vertical) with no visible text or chevron.
label still supplies the trigger's accessible name via
aria-label — useful for a compact per-row action menu.
<mosni-dropdown label="Actions">
<mosni-dropdown-item value="rename">Rename</mosni-dropdown-item>
<mosni-dropdown-item value="duplicate">Duplicate</mosni-dropdown-item>
<mosni-dropdown-item value="delete" variant="danger"
>Delete</mosni-dropdown-item
>
</mosni-dropdown>
<p>
Click the trigger to open, use <kbd>↑</kbd>/<kbd>↓</kbd> to move between
items, <kbd>Esc</kbd> or an outside click to dismiss. Selecting an item fires
<code>mosni-dropdown-select</code> with <code>detail: { value }</code>.
</p>
<mosni-dropdown icon-only="more-vertical" label="Actions for report.pdf">
<mosni-dropdown-item value="rename">Rename</mosni-dropdown-item>
<mosni-dropdown-item value="duplicate">Duplicate</mosni-dropdown-item>
<mosni-dropdown-item value="delete" variant="danger"
>Delete</mosni-dropdown-item
>
</mosni-dropdown>
<p>
With the <code>icon-only</code> attribute, the trigger renders just the named
glyph (default <code>more-vertical</code>) with no visible text or chevron.
<code>label</code> still supplies the trigger's accessible name via
<code>aria-label</code> — useful for a compact per-row action menu.
</p>| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
label |
string | no | — |
Trigger button text; mirrored property. |
| Name | Description |
|---|---|
mosni-dropdown-select |
Bubbles when an item is chosen; detail: { value }. |
We'll never share your email.
Enter a valid email address.
import { Field } from "@mosni/react";
export default function Example() {
return (
<>
<Field
label="Email"
type="email"
required
help="We'll never share your email."
/>
<Field label="Display name">
<input type="text" placeholder="Jane Doe" />
</Field>
<Field
label="Email"
type="email"
defaultValue="not-an-email"
error="Enter a valid email address."
/>
</>
);
}<mosni-field label="Email" type="email" required help="We'll never share your email." ></mosni-field> <mosni-field label="Display name"> <input type="text" placeholder="Jane Doe" /> </mosni-field> <mosni-field label="Email" type="email" value="not-an-email" error="Enter a valid email address." ></mosni-field>
| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
label |
string | no | — |
Field label text. |
type |
string | no | text |
Control kind when generating: text (default)/password/email/number/url/search/tel/date/textarea/select/checkbox/radio. |
name |
string | no | — |
Control name (form submission). |
value |
string | no | — |
Initial value. |
required |
boolean | no | false |
Adds the * marker + control required. |
help |
string | no | — |
Help text under the control. |
error |
string | yes | — |
Error message; presence sets .field.error + aria-invalid + shows the message. |
| Name | Description |
|---|---|
change |
Native change from the (authored or generated) control; bubbles from light DOM already. |
input |
Native input from the (authored or generated) control; bubbles from light DOM already. |
import { Header } from "@mosni/react";
export default function Example() {
return (
<Header
brand="MOSNI'S"
accent="HEADER"
tagline={
<span>
made with love by <a href="https://mosni.dev">mosni</a>
</span>
}
/>
);
}<mosni-header brand="MOSNI'S" accent="HEADER">
<span slot="tagline"
>made with love by <a href="https://mosni.dev">mosni</a></span
>
</mosni-header><header class="header">
<div class="brand">MOSNI'S <span class="purple">HEADER</span></div>
<div class="little-link">
made with love by <a href="https://mosni.dev">mosni</a>
</div>
</header>| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
brand |
string | no | — |
Brand text (left). Plain leading text of the brand. |
accent |
string | no | — |
Optional trailing brand text wrapped in .purple (exordium's split-brand look). |
href |
string | no | / |
Target for the brand link. The brand always links (logo + text together); defaults to / (home). |
tagline |
string | no | — |
Plain text for the right-side little-link region. |
no-logo |
boolean | no | false |
Suppress the mosni logo, which is shown left of the brand by default. |
| Name | Description |
|---|---|
brand |
Rich brand content overriding brand/accent (always wrapped in the brand link — see href). |
tagline |
Rich right-side content overriding tagline. |
Main content goes here.
import {
Header,
Layout,
Menu,
MenuItem,
} from "@mosni/react";
export default function Example() {
return (
<Layout
header={<Header brand="MOSNI'S" accent="APP" />}
menu={
<Menu label="Primary">
<MenuItem
title="Overview"
subtitle="Summary and status"
href="#"
selected
/>
<MenuItem title="Settings" subtitle="Preferences" href="#" />
</Menu>
}
>
<p>Main content goes here.</p>
</Layout>
);
}Main content goes here.
<mosni-layout>
<mosni-header slot="header" brand="MOSNI'S" accent="APP"></mosni-header>
<mosni-menu slot="menu" label="Primary">
<mosni-menu-item
title="Overview"
subtitle="Summary and status"
href="#"
selected
></mosni-menu-item>
<mosni-menu-item
title="Settings"
subtitle="Preferences"
href="#"
></mosni-menu-item>
</mosni-menu>
<p>Main content goes here.</p>
</mosni-layout>Main content goes here.
<div class="layout">
<header class="header">
<div class="brand">MOSNI'S <span class="purple">APP</span></div>
</header>
<div class="layout-menu">
<nav class="menu">
<a class="menu-entry selected">
<span class="menu-entry-title">Overview</span>
<span class="menu-entry-subtitle">Summary and status</span>
</a>
<a class="menu-entry">
<span class="menu-entry-title">Settings</span>
<span class="menu-entry-subtitle">Preferences</span>
</a>
</nav>
</div>
<main class="layout-main">
<p>Main content goes here.</p>
</main>
</div>| Name | Description |
|---|---|
header |
A <mosni-header> or any header. |
menu |
A <mosni-menu>. |
footer |
An optional <mosni-footer>. |
import { Lightbox } from "@mosni/react";
export default function Example() {
return (
<Lightbox src="mosnicat.png" alt="Meow?" caption="The cat" width={240} />
);
}
<mosni-lightbox caption="The cat"> <img src="mosnicat.png" alt="Meow?" width="240" /> </mosni-lightbox>
| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
full |
string | no | thumbnail src |
Full-resolution src for the overlay (defaults to the thumbnail's src). |
caption |
string | no | — |
Optional caption under the enlarged image. |
| Name | Description |
|---|---|
close |
Native <dialog> close on the generated overlay. |
The mosni mark. Size it by setting height (or font-size) — it keeps its aspect ratio at any scale:
import { Logo } from "@mosni/react";
export default function Example() {
return (
<>
<p>
The mosni mark. Size it by setting <code>height</code> (or{" "}
<code>font-size</code>) — it keeps its aspect ratio at any scale:
</p>
<div style={{ display: "flex", alignItems: "flex-end", gap: "1.5rem" }}>
<Logo alt="mosni" style={{ height: "1.5rem" }} />
<Logo alt="mosni" style={{ height: "3rem" }} />
<Logo alt="mosni" style={{ height: "5rem" }} />
</div>
</>
);
}
The mosni mark. Size it by setting height (or
font-size) — it keeps its aspect ratio at any scale:
<p> The mosni mark. Size it by setting <code>height</code> (or <code>font-size</code>) — it keeps its aspect ratio at any scale: </p> <div style="display: flex; align-items: flex-end; gap: 1.5rem"> <mosni-logo style="height: 1.5rem"></mosni-logo> <mosni-logo style="height: 3rem"></mosni-logo> <mosni-logo style="height: 5rem"></mosni-logo> </div>
| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
alt |
string | no | mosni |
Alt text for the logo image. |
import { useState } from "react";
import { Modal } from "@mosni/react";
// The dialog itself is portalled to document.body (agent-docs → planning-artifacts/react-path-implementation-waves.md §10) - under the static render
// this docs page uses, that portal renders nothing (there is no live document.body to attach to
// during renderToStaticMarkup), so the demo pane below only ever shows the trigger button. That is
// the real, accurate output of this component under static rendering, not a stand-in.
export default function Example() {
const [open, setOpen] = useState(false);
return (
<div className="panel">
<button className="btn" onClick={() => setOpen(true)}>
Open modal
</button>
<Modal
open={open}
onClose={() => setOpen(false)}
heading="Delete project?"
footer={
<>
<button className="btn" onClick={() => setOpen(false)}>
Cancel
</button>
<button className="btn" onClick={() => setOpen(false)}>
Delete
</button>
</>
}
>
<p>This action can't be undone.</p>
</Modal>
</div>
);
}This action can't be undone.
<div class="panel">
<button class="btn" onclick="this.nextElementSibling.show()">
Open modal
</button>
<mosni-modal heading="Delete project?">
<p>This action can't be undone.</p>
<button slot="footer" class="btn">Cancel</button>
<button slot="footer" class="btn">Delete</button>
</mosni-modal>
</div>| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
open |
boolean | yes | false |
Open state - drives dialog.showModal()/close(); reflects back on native close. |
heading |
string | no | — |
Dialog heading (<h1 class="modal-heading">). |
| Name | Description |
|---|---|
heading |
Rich heading, overrides the heading attribute. |
footer |
The action button row. |
| Name | Description |
|---|---|
close |
Native <dialog> close (dialog.returnValue carries the value). |
cancel |
Native <dialog> cancel (Esc). |
.text-container is the same as .content-container. Kept around for backwards compatibility reasons.
Use your passkey to continue.
import { Panel } from "@mosni/react";
export default function Example() {
return (
<Panel heading="Sign in">
<p>Use your passkey to continue.</p>
</Panel>
);
}Use your passkey to continue.
<mosni-panel heading="Sign in"> <p>Use your passkey to continue.</p> </mosni-panel>
Use your passkey to continue.
<div class="panel"> <h1>Sign in</h1> <p>Use your passkey to continue.</p> </div>
<div class="panel"> <input type="email" placeholder="you@example.com" /> <input type="password" placeholder="Password" /> </div>
This is an element of class "text-container".
<div class="text-container"> <p>This is an element of class "text-container".</p> </div>
| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
heading |
string | no | — |
Injected as <h1> unless an authored heading child is present. |
size |
string | no | large |
Width: large (default, mirrors .text-container)/small (narrow card)/full (full width). |
| Name | Description |
|---|---|
heading |
Enhance-first: an authored <h1> or slot="heading" child always wins over the heading attribute. |
import { Slider } from "@mosni/react";
export default function Example() {
return (
<Slider
stops={[
"30 minutes",
"1 hour",
"2 hours",
"6 hours",
"12 hours",
"24 hours",
"2 days",
"7 days",
"30 days",
"90 days",
]}
defaultValue={1}
label="Link expires after"
/>
);
}<mosni-slider stops="30 minutes|1 hour|2 hours|6 hours|12 hours|24 hours|2 days|7 days|30 days|90 days" value="1" label="Link expires after" ></mosni-slider>
| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
stops |
string | yes | — |
Pipe-delimited stop labels in order, e.g. "30 minutes|1 hour|2 hours". Only the first and last are printed under the track ends; the readout names the current stop. |
value |
number | yes | 0 |
Selected stop INDEX (not the stop's meaning). Reflected onto self before change bubbles past this element; mirrored property. |
label |
string | yes | — |
Optional caption above the track; also becomes the range input's accessible name. |
| Name | Description |
|---|---|
change |
Native change, bubbling from the inner range input; no custom event. value already reflects the new index by the time this reaches an ancestor. |
import { Switch } from "@mosni/react";
export default function Example() {
return <Switch label="Enable notifications" defaultChecked />;
}<mosni-switch label="Enable notifications" checked></mosni-switch>
| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
checked |
boolean | yes | false |
On/off; mirrors the inner checkbox. |
disabled |
boolean | yes | false |
Disabled. |
label |
string | no | — |
Optional inline label (clickable). |
name |
string | no | — |
Form field name. |
value |
string | no | — |
Submitted value when checked. |
| Name | Description |
|---|---|
change |
Native change; bubbles from the inner checkbox. |
The chrome ships components and utility classes from one file.
Add one script tag to your page's head.
Override the CSS custom properties in :root.
import { Tab, Tabs } from "@mosni/react";
export default function Example() {
return (
<Tabs>
<Tab label="Overview">
<p>The chrome ships components and utility classes from one file.</p>
</Tab>
<Tab label="Installation">
<p>Add one script tag to your page's head.</p>
</Tab>
<Tab label="Theming">
<p>Override the CSS custom properties in :root.</p>
</Tab>
</Tabs>
);
}The chrome ships components and utility classes from one file.
Add one script tag to your page's head.
Override the CSS custom properties in :root.
<mosni-tabs>
<mosni-tab label="Overview" selected>
<p>The chrome ships components and utility classes from one file.</p>
</mosni-tab>
<mosni-tab label="Installation">
<p>Add one script tag to your page's head.</p>
</mosni-tab>
<mosni-tab label="Theming">
<p>Override the CSS custom properties in :root.</p>
</mosni-tab>
</mosni-tabs>| Name | Description |
|---|---|
mosni-tab-change |
Bubbles on selection change; detail: { index, label }. Also settable via tabs.selectedIndex. |
import { useToast } from "@mosni/react";
export default function Example() {
const toast = useToast();
return (
<div className="panel">
<button
className="btn"
onClick={() => toast("Saved successfully.", { variant: "success" })}
>
Show toast
</button>
</div>
);
}<div class="panel">
<button
class="btn"
onclick="window.mosni.toast('Saved successfully.', { variant: 'success' })"
>
Show toast
</button>
</div>
<!-- Declarative form: <mosni-toast variant="success">Saved successfully.</mosni-toast> -->| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
variant |
string | no | info |
info (default)/success/error - read once on connect, forwarded to the created toast. |
| Name | Description |
|---|---|
mosni-toast-dismiss |
Bubbles from a toast element when it is dismissed (button, timeout, or handle.dismiss()). |
Paste the key into the request header to authenticate.
import { Tooltip } from "@mosni/react";
export default function Example() {
return (
<p>
Paste the key into the request{" "}
<Tooltip text="Copies the API key to your clipboard.">
<span className="tooltip-trigger" tabIndex={0}>
header
</span>
</Tooltip>{" "}
to authenticate.
</p>
);
}
Paste the key into the request
<p>
Paste the key into the request
<mosni-tooltip text="Copies the API key to your clipboard.">
<span class="tooltip-trigger" tabindex="0">header</span>
</mosni-tooltip>
to authenticate.
</p>| Name | Type | Observed | Default | Description |
|---|---|---|---|---|
text |
string | yes | — |
Tooltip text (ignored once a slot="tip" child is used); mirrored property. |
| Name | Description |
|---|---|
tip |
Rich tip content, overrides the text attribute. |
<div class="progress" style="--progress: 60%"></div>
Mosni-Chrome is the shared visual chrome for every app on Hannah's stack.
Flat colours, few gradients.
<div class="prose">
<h2>About Mosni-Chrome</h2>
<p>
Mosni-Chrome is the shared visual chrome for every app on Hannah's stack.
</p>
<blockquote>Flat colours, few gradients.</blockquote>
</div>The .purple utility applies the mosnicat accent colour to any inline element.
<p> The <span class="purple">.purple</span> utility applies the mosnicat accent colour to any inline element. </p>
<span class="spinner" role="status" aria-label="Loading"></span>
Passkey verified.
<div class="panel"> <p class="status">Passkey verified.</p> </div>
| Name | Role | Status |
|---|---|---|
| Hannah | Hello | Active |
| Second row | Hi | Active |
.table-col-secondary on both a column's
<th> and its <td>s hides that column
below 480px, rather than letting the table (or the page) scroll sideways.
Resize below 480px to see the "Role" column above disappear. The
.table-scroll wrapper is the backstop for whatever
.table-col-secondary doesn't fully cover - the table can still
scroll on its own, but the page it's on never widens because of it.
<div class="table-scroll">
<table class="table interactive">
<thead>
<tr>
<th>Name</th>
<th class="table-col-secondary">Role</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hannah</td>
<td class="table-col-secondary">Hello</td>
<td>Active</td>
</tr>
<tr>
<td>Second row</td>
<td class="table-col-secondary">Hi</td>
<td>Active</td>
</tr>
</tbody>
</table>
</div>
<p>
<code>.table-col-secondary</code> on both a column's
<code><th></code> and its <code><td></code>s hides that column
below 480px, rather than letting the table (or the page) scroll sideways.
Resize below 480px to see the "Role" column above disappear. The
<code>.table-scroll</code> wrapper is the backstop for whatever
<code>.table-col-secondary</code> doesn't fully cover - the table can still
scroll on its own, but the page it's on never widens because of it.
</p>
@mosni/react is a third authoring path for mosnicat, alongside
the <mosni-*> custom elements and plain HTML/classes.
Components render the same DOM directly in React, with no custom element in
the loop. No ownership conflicts, no upgrade timing, correct SSR on first
paint.
Not published to a registry. Install straight from the tarball URL: npm records the resolved URL and an integrity hash in the lockfile, so it stays pinned and reproducible.
"@mosni/react": "https://ui.mosni.dev/mosni-react-0.1.0.tgz"
react >= 18 is a peer dependency. Bring your own.
The React package ships no CSS. Keep the mosnicat bootstrap script for styling, fonts, favicon, and viewport. A theme or colour change still reaches every app without a redeploy:
<script src="https://mosni.dev/mosnicat.js"></script>
import { Panel, Field } from "@mosni/react".
no-logo → noLogo, filter-threshold →
filterThreshold.
ReactNode and
covers both: <Header brand="MOSNI'S"> and
<Header brand={<img />}>
are both valid.
on* callbacks: mosni-tab-change →
onChange(index, label), mosni-dropdown-select →
onSelect(value).
selectedIndex/defaultSelectedIndex,
open/defaultOpen,
checked/defaultChecked,
value/defaultValue.
ref forwards to the root DOM element; className is
merged with the component's own classes, never replaced.
Add JSX.IntrinsicElements declarations for every
<mosni-*> tag with one import. Delete any hand-maintained
declare module "react" { namespace JSX { … } } block doing the
same job:
import "@mosni/react/elements";
This type-checks existing <mosni-*> markup with zero
behavioural change. Adopt individual <X> React components
where the ownership bugs actually bite.
Every component is SSR-safe: nothing reads window or
document at module scope, and generated ids use
useId() instead of a module-level counter that would desync
server and client. React 19 also hoists a
<link rel="preload" as="image"> ahead of any component
containing an <img> (<Logo>, and by
extension <Header>/<Layout>). That's
expected output, not a bug.
<Modal>, <Tooltip>, and
<Lightbox>'s open overlay portal their floating content to
document.body, which doesn't exist during SSR. Until the client
mounts, these render only their non-portalled part: a modal's trigger button,
for example.
Component markup updates only when the app bumps the
@mosni/react package version; CSS still updates live from the CDN
bootstrap. The class path has the same property. Only the custom-element path
picks up markup changes without a redeploy.