Hannah's design library

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>

Alert

Changes saved.
<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>

Badge

New Active Failed

<p>
  <span class="badge primary">New</span>
  <span class="badge success">Active</span>
  <span class="badge error">Failed</span>
</p>

Button

<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>

Content Container

Headline

Some text very interesting.

<div class="content-container">
  <h2>Headline</h2>
  <p>Some text very interesting.</p>
</div>

Divider

Above the divider.


Below the divider.

<p>Above the divider.</p>
<hr class="divider" />
<p>Below the divider.</p>

Icons

rocket
heart
star
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:

rocket
heart
star
<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>


Components

Either use these as components, custom element, or as plain html with classes

Examples include tabs for both versions when available.


Mosni Accordion

What is Mosni-Chrome?

The shared visual chrome for every app on the Hannah's stack.

How do I include it?

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>
  );
}
What is Mosni-Chrome?

The shared visual chrome for every app on the Hannah's stack.

How do I include it?

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>

Attributes

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>.

Events

Name Description
toggle Native toggle, bubbling from each child <details>.

Mosni Chips

Roles
files:readstream:key
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>

Attributes

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.

Events

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.

Mosni Code

ts
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>

Attributes

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.

Mosni Dropdown

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>
    </>
  );
}
Rename Duplicate Delete

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 }.

Rename Duplicate Delete

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>

Attributes

Name Type Observed Default Description
label string no — Trigger button text; mirrored property.

Events

Name Description
mosni-dropdown-select Bubbles when an item is chosen; detail: { value }.

Mosni Field

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>

Attributes

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.

Events

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.


Header

MOSNI'S HEADER
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>
      }
    />
  );
}
made with love by mosni
<mosni-header brand="MOSNI'S" accent="HEADER">
  <span slot="tagline"
    >made with love by <a href="https://mosni.dev">mosni</a></span
  >
</mosni-header>
MOSNI'S 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>

Attributes

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.

Slots

Name Description
brand Rich brand content overriding brand/accent (always wrapped in the brand link — see href).
tagline Rich right-side content overriding tagline.

Layout

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>
MOSNI'S APP

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>

Slots

Name Description
header A <mosni-header> or any header.
menu A <mosni-menu>.
footer An optional <mosni-footer>.

Mosni Lightbox

Meow?
import { Lightbox } from "@mosni/react";

export default function Example() {
  return (
    <Lightbox src="mosnicat.png" alt="Meow?" caption="The cat" width={240} />
  );
}
Meow?
<mosni-lightbox caption="The cat">
  <img src="mosnicat.png" alt="Meow?" width="240" />
</mosni-lightbox>

Attributes

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.

Events

Name Description
close Native <dialog> close on the generated overlay.


Mosni Menu

import { Menu, MenuItem } from "@mosni/react";

export default function Example() {
  return (
    <Menu label="Primary">
      <MenuItem
        title="Overview"
        subtitle="Summary and status"
        href="#"
        selected
      />
      <MenuItem title="Settings" subtitle="Preferences" href="#" />
    </Menu>
  );
}
<mosni-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>

Attributes

Name Type Observed Default Description
label string no — Sets aria-label on the nav (role=navigation).

Mosni Modal

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>

Attributes

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">).

Slots

Name Description
heading Rich heading, overrides the heading attribute.
footer The action button row.

Events

Name Description
close Native <dialog> close (dialog.returnValue carries the value).
cancel Native <dialog> cancel (Esc).

Panel & containers

.text-container is the same as .content-container. Kept around for backwards compatibility reasons.

Sign in

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>

Sign in

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>

Attributes

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).

Slots

Name Description
heading Enhance-first: an authored <h1> or slot="heading" child always wins over the heading attribute.

Mosni Slider

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>

Attributes

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.

Events

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.

Mosni Switch

import { Switch } from "@mosni/react";

export default function Example() {
  return <Switch label="Enable notifications" defaultChecked />;
}
<mosni-switch label="Enable notifications" checked></mosni-switch>

Attributes

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.

Events

Name Description
change Native change; bubbles from the inner checkbox.

Mosni Tabs

The chrome ships components and utility classes from one file.

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>

Events

Name Description
mosni-tab-change Bubbles on selection change; detail: { index, label }. Also settable via tabs.selectedIndex.

Mosni Toast

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> -->

Attributes

Name Type Observed Default Description
variant string no info info (default)/success/error - read once on connect, forwarded to the created toast.

Events

Name Description
mosni-toast-dismiss Bubbles from a toast element when it is dismissed (button, timeout, or handle.dismiss()).

Mosni Tooltip

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 header to authenticate.

<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>

Attributes

Name Type Observed Default Description
text string yes — Tooltip text (ignored once a slot="tip" child is used); mirrored property.

Slots

Name Description
tip Rich tip content, overrides the text attribute.

Progress

<div class="progress" style="--progress: 60%"></div>

Prose

About Mosni-Chrome

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>

Purple

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>

Spinner

<span class="spinner" role="status" aria-label="Loading"></span>

Status

Passkey verified.

<div class="panel">
  <p class="status">Passkey verified.</p>
</div>

Table

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>&lt;th&gt;</code> and its <code>&lt;td&gt;</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>

React

@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.

Install

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.

Keep the bootstrap script tag

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>

Conventions

  • Named exports, PascalCase: import { Panel, Field } from "@mosni/react".
  • Props are camelCase counterparts of the documented kebab attributes: no-logo → noLogo, filter-threshold → filterThreshold.
  • Booleans are real booleans, never presence-strings.
  • A slot with a plain-text attribute twin takes a ReactNode and covers both: <Header brand="MOSNI'S"> and <Header brand={<img />}> are both valid.
  • Events become on* callbacks: mosni-tab-change → onChange(index, label), mosni-dropdown-select → onSelect(value).
  • Every stateful component supports both controlled and uncontrolled use, React-idiomatically: 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.

Types-only step (no runtime change)

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.

SSR

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.

The version-locking caveat

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.

made with love by mosni.dev