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-logonoLogo, filter-thresholdfilterThreshold.
  • 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-changeonChange(index, label), mosni-dropdown-selectonSelect(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