npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@matthiaskrijgsman/mat-ui

v0.0.73

Published

[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Readme

mat-ui

License: MIT

A React component library providing UI primitives built with React 19, Tailwind CSS v4, and Floating UI.

Showcase

View the components here

Development

The repo is a pnpm workspace with two packages: the library at the repository root and the showcase Next.js app in site/.

# install workspace deps
pnpm install

# run the showcase against the local library
pnpm site            # → http://localhost:6006

# build the library
pnpm build

# build the showcase as a static site (outputs to site/out)
pnpm site:build

The showcase pulls in the library via the workspace alias (workspace:*), so edits under src/ are reflected on the next dev rebuild. Deployments to GitHub Pages happen via .github/workflows/deploy-site.yml.

Authoring rule: every Tailwind utility in src/ is written with the mat: prefix — mat:flex, mat:hover:ring-2, mat:-translate-y-1/2 — and so is every @apply. An unprefixed utility compiles to nothing (Tailwind only accepts prefixed candidates once a prefix is set), and pnpm build runs scripts/check-css.mjs, which fails on any unprefixed class in the utilities layer. Authored component classes (.button-primary, .input-base, …) are not utilities and stay as they are.

Installation

pnpm install @matthiaskrijgsman/mat-ui

Styles

Import the mat-ui stylesheet in your CSS entry file. On a Tailwind v4 host, do this after the Tailwind import.

@import "@matthiaskrijgsman/mat-ui/style";

The stylesheet is Tailwind v4 output with every utility prefixed (mat:flex compiles to .mat\:flex, the theme variables to --mat-*), so nothing in it can collide with your own Tailwind classes, whatever version you run. It ships no global preflight; the few resets the components need are scoped to their own classes.

Not on Tailwind v4? ./style uses native cascade layers, and in a host whose own CSS is unlayered (Tailwind v3, or no Tailwind) every layered rule loses to every host rule — your reset's input { padding: 0 } would beat our inputs' padding. A v3 PostCSS pipeline also refuses to import a file with bare @layer blocks. Import the flat entry instead, and mark the subtree that uses mat-ui components:

@import "@matthiaskrijgsman/mat-ui/style-flat";
<div class="mat-ui">…your app…</div>   <!-- or data-mat-ui -->

style-flat is the same rules with the layers flattened and every selector scoped to .mat-ui / [data-mat-ui] (plus Floating UI's portal, where menus, tooltips and modals render), at zero extra specificity. Its :root / .dark token declarations drop to zero specificity too, so a same-named custom property of your own always wins. Wrap your app root, or just the region that renders mat-ui — anything outside the wrapper is untouched by the stylesheet.

Usage

import { Button } from "@matthiaskrijgsman/mat-ui";

function App() {
  return <Button variant={'primary'}>Click me</Button>;
}

Dark Theme

mat-ui ships with built-in dark theme support. Add the dark class to the <html> element to activate it:

<html class="dark">

Toggle it with JavaScript:

// Manual toggle
document.documentElement.classList.toggle('dark');

// Follow OS preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.toggle('dark', prefersDark);

All components adapt automatically — no additional props or configuration needed.

Theming with design tokens

Every visual property in mat-ui — color, shape, type, elevation and size — is driven by CSS custom properties (design tokens) defined on :root. There is no Tailwind config to fork and no component props to thread: you retheme the whole kit by overriding tokens in your own CSS, after importing the stylesheet.

@import "@matthiaskrijgsman/mat-ui/style";

:root {
  --color-button-primary-bg: #0ea5e9;   /* brand color */
  --border-radius-input: 0px;            /* square corners everywhere */
  --font-weight-button: 700;             /* bolder buttons */
}

How the tokens are organised

Tokens fall into three families:

  • Structure — typography (family, weight, size), border radius, border width, shadow, ring (focus/hover) width and transition timing. Theme-independent.
  • Sizing — the shared sm | md | lg control scale (height, padding, gap, icon size).
  • Color — every surface, border, text and state color. Defined on :root (light) and overridden under .dark.

Structure and color tokens use a two-tier model:

  1. Base scales — a small set of primitives (e.g. --font-weight-strong, --radius-xl). Change one to shift the whole kit at once.
  2. Semantic aliases — per-component tokens that point at the base scale by default (e.g. --font-weight-button: var(--font-weight-strong), --border-radius-dropdown: var(--mat-radius-xl)). Override one to retheme a single component without touching anything else.

So --font-weight-strong: 700 makes every emphasised element heavier, while --font-weight-button: 700 changes only buttons. Pick the tier that matches how broad your change is.

Dark mode: add the dark class to <html> (see Dark Theme). Only color tokens differ between themes — structure and sizing are shared, so you never duplicate them under .dark.

Worked examples

Square, flat, heavier — in four tokens:

:root {
  --border-radius-input: 0px;       /* inputs, selects, buttons */
  --border-radius-panel: 0.25rem;   /* panels, modals */
  --border-width-input: 2px;        /* all control & surface borders */
  --font-weight-strong: 700;        /* buttons, badges, tabs, dropdown items… */
}

Set the kit's typeface in one place:

:root {
  --font-family-base: "Inter", system-ui, sans-serif;
}

Token reference

Structure — typography

Font weights resolve through a three-step base scale; the semantic tokens below point at it by default.

| Base token | Default | |------------|---------| | --font-weight-normal | 400 | | --font-weight-medium | 500 | | --font-weight-strong | 600 |

| Semantic token | Applies to | Default | |----------------|-----------|---------| | --font-weight-input-text | Text typed into inputs, selects, textareas | --font-weight-normal | | --font-weight-button | Button, ButtonIconSquare, ButtonIconRound, file-input action text | --font-weight-strong | | --font-weight-badge | Badge | --font-weight-strong | | --font-weight-tab | TabButtons | --font-weight-strong | | --font-weight-tab-count | TabButtons / Tabs count chip | --font-weight-strong | | --font-weight-tabs | Tabs (underline) tab labels | --font-weight-strong | | --font-weight-input-label | InputLabel (label above inputs) | --font-weight-medium | | --font-weight-input-description | InputDescription | --font-weight-medium | | --font-weight-input-error | InputError | --font-weight-medium | | --font-weight-input-option-label | Inline labels on InputCheck / InputRadio / InputToggle, file tile names | --font-weight-medium | | --font-weight-dropdown-item | DropdownButton, PanelLink | --font-weight-strong | | --font-weight-group-header | Dropdown group labels, select group headers | --font-weight-strong | | --font-weight-panel-field | PanelField label | --font-weight-medium | | --font-weight-panel-link | PanelLink | --font-weight-strong | | --font-weight-table-header | Table header cells, TableEmpty title | --font-weight-medium | | --font-weight-table-cell | Table body cells | --font-weight-normal | | --font-weight-calendar-title | Calendar header (month / year) and its "Today" button | --font-weight-strong | | --font-weight-calendar-cell | Calendar days, months, years, weekday names and the time options | --font-weight-medium |

| Token | Description | Default | |-------|-------------|---------| | --font-family-base | Typeface for all kit text (defaults to the host font) | inherit | | --font-family-numeric | Number inputs (type="number") and the rich-text toolbar's numeric fields, rendered with tabular figures — point it at a mono stack if your numerals live there | var(--font-family-base) | | --font-size-label | Dropdown / select group label size | var(--mat-text-sm) | | --font-size-description | InputDescription and PanelField label size | var(--mat-text-sm) | | --font-size-error | InputError size | var(--mat-text-sm) | | --font-size-tab-count | TabButtons / Tabs count chip size | var(--mat-text-xs) | | --font-size-calendar-title | Calendar header and "Today" button size | var(--mat-text-sm) | | --font-size-calendar-cell | Calendar days, months, years and time options | var(--mat-text-sm) | | --font-size-calendar-weekday | Calendar weekday names | var(--mat-text-xs) | | --font-size-tooltip | Tooltip text size — opt-in: undeclared by default, the tooltip inherits the surrounding text size | (inherit) | | --font-weight-tooltip | Tooltip text weight — opt-in: undeclared by default, the tooltip inherits the surrounding weight | (inherit) |

The text size of the input/button itself comes from the control sizing scale below (--control-size-{size}-font-size), not from these tokens.

Structure — border radius

Semantic radius tokens map onto Tailwind's radius scale. Override a token to change one group; override the underlying --mat-radius-* (Tailwind's scale, prefixed) to change several at once.

| Token | Applies to | Default | |-------|-----------|---------| | --border-radius-input | Text inputs, selects, textareas, file inputs, the Lexical editor box | var(--mat-radius-xl) | | --border-radius-button | Button, ButtonIconSquare (and the file-input "Choose" button) | var(--border-radius-input) | | --border-radius-panel | Panel, PanelStack, Modal, TableEmpty icon frame | var(--mat-radius-2xl) | | --border-radius-dropdown | DropdownPanel, Lexical floating toolbar | var(--mat-radius-xl) | | --border-radius-option | Select option rows | var(--mat-radius-xl) | | --border-radius-menu-item | DropdownButton, PanelLink, Lexical toolbar buttons | var(--mat-radius-lg) | | --border-radius-badge | Badge | var(--mat-radius-lg) | | --border-radius-tab | TabButtons container | var(--mat-radius-xl) | | --border-radius-tab-inner | TabButtons pills — set to calc(var(--border-radius-tab) - var(--tab-container-padding)) for a concentric look | var(--border-radius-tab) | | --border-radius-tooltip | Tooltip panel | var(--border-radius-dropdown) | | --border-radius-checkbox | InputCheck box | var(--mat-radius-lg) | | --border-radius-control-inner | Color swatch and picker bars in InputColor | var(--mat-radius-md) | | --border-radius-calendar-cell | Calendar days, months, years, header buttons and time options | var(--border-radius-menu-item) |

ButtonIconRound, the toggle track/thumb, and radio dots are intentionally fully round (rounded-full) and are not tokenized.

Structure — border width & shadow

| Token | Applies to | Default | |-------|-----------|---------| | --border-width-input | Border width of inputs, selects, buttons, panels, dropdowns, modals, check/radio, the Tabs bottom rule | 1px | | --border-width-tabs-indicator | Active-tab underline in Tabs | 2px | | --border-width-tooltip | Tooltip panel border width | var(--border-width-input) | | --shadow-control | Resting elevation of buttons, inputs, panels, tabs | var(--mat-shadow-sm) | | --shadow-dropdown | DropdownPanel and the Lexical floating toolbar | var(--mat-shadow-lg) | | --shadow-overlay | Modal and SidebarModal | var(--mat-shadow-xl) | | --shadow-tooltip | Tooltip panel | var(--shadow-dropdown) |

Structure — component geometry

| Token | Applies to | Default | |-------|-----------|---------| | --tab-container-padding | Inset between the TabButtons container edge and the pills | 0.25rem | | --tab-container-gap | Gap between TabButtons pills | 0.25rem | | --tooltip-padding-x | Tooltip panel horizontal padding | 0.75rem | | --tooltip-padding-y | Tooltip panel vertical padding | 0.75rem | | --calendar-cell-size | Edge of one Calendar day cell — the month grid is seven of these wide, the time options one high | 2.25rem |

Structure — ring & transition

Controls share a consistent interaction model: a focus/hover "glow" ring, a thinner inset ring on press, and a single transition duration. The ring color comes from the per-component --color-*-ring tokens (see the color sections); these set its width and the animation timing.

| Token | Applies to | Default | |-------|-----------|---------| | --control-ring-width | Ring width on hover, focus, focus-within, and the select/dropzone open state | 4px | | --control-ring-width-active | Ring width on press (:active) | 1px | | --control-transition-duration | Duration of hover/focus/press transitions on buttons, inputs, selects, the icon-button press scale, etc. | 150ms | | --control-transition-duration-fast | Quicker color-only transitions (table row/header hover, clickable Badge) | 100ms |

The resting state is always ringless (ring-0) and a few elements opt out of a focus ring entirely (dropdown items, tabs) — these are intentional and not tokenized.

Control sizing

Button, ButtonIconSquare, ButtonIconRound, Input, InputColor, InputDate, InputDateTime, InputRange, InputTextArea, InputSelectNative, InputSelect, InputSelectSearchable, and InputSelectSearchableAsync accept a size?: 'sm' | 'md' | 'lg' prop (default 'md') and read their dimensions from a single shared scale. Override these to adjust heights, padding, font size, and icon sizing consistently across all controls. Replace {size} with sm, md, or lg.

| Token | Description | sm · md · lg defaults | |-------|-------------|------------------------| | --control-size-{size}-height | Control height (also width for square/round icon buttons) | 2.5rem · 3rem · 3.5rem | | --control-size-{size}-px | Horizontal padding | 1rem · 1rem · 1.25rem | | --control-size-{size}-gap | Gap between icon and label inside buttons | 0.5rem · 0.5rem · 0.75rem | | --control-size-{size}-font-size | Text size | 1rem · 1rem · 1rem | | --control-size-{size}-icon | Icon glyph size inside controls (also the InputRange thumb size) | 1rem · 1.25rem · 1.5rem | | --control-size-{size}-icon-offset | Distance from the input edge to a leading icon (used when an Icon prop is set on Input) | 1rem · 1rem · 1.25rem | | --control-size-{size}-range-track | InputRange track thickness | 0.375rem · 0.5rem · 0.625rem | | --control-size-{size}-tab-height | TabButtons container height | var(--control-size-{size}-height) | | --control-size-{size}-tab-px | Horizontal padding inside each TabButtons pill | var(--control-size-{size}-px) | | --control-size-{size}-tab-font-size | TabButtons label text size | var(--control-size-{size}-font-size) |

Color — focus ring

| Token | Description | Light default | |-------|-------------|---------------| | --color-input-focus-ring | Focus ring color for buttons and inputs | rgb(17 24 39 / 0.15) |

Color — buttons

Each button variant (primary, white, black, transparent, secondary, tertiary) uses the same set of tokens. Replace {variant} with the variant name.

| Token | Description | |-------|-------------| | --color-button-{variant}-bg | Background color | | --color-button-{variant}-bg-hover | Background on hover | | --color-button-{variant}-bg-active | Background on press | | --color-button-{variant}-border | Border color | | --color-button-{variant}-border-hover | Border on hover | | --color-button-{variant}-border-active | Border on press | | --color-button-{variant}-text | Text color | | --color-button-{variant}-text-active | Text color on press | | --color-button-{variant}-bg-disabled | Background when disabled | | --color-button-{variant}-border-disabled | Border when disabled | | --color-button-{variant}-text-disabled | Text color when disabled |

Color — inputs

| Token | Description | Light default | |-------|-------------|---------------| | --color-input-bg | Input background | #ffffff | | --color-input-border | Input border | #e5e7eb | | --color-input-text | Input text color | #111827 | | --color-input-placeholder | Placeholder text color | #9ca3af | | --color-input-ring | Ring color on hover | rgb(17 24 39 / 0.1) | | --color-input-border-error | Border color in error state | #dc2626 | | --color-input-ring-error | Ring color in error state | rgb(220 38 38 / 0.2) | | --color-input-icon | Leading icon color | rgb(17 24 39 / 0.6) |

Field-like inputs (Input, InputPassword, InputTextArea, InputColor, InputDate, InputDateTime, InputLexical, InputFileSingle, and the whole select family) also accept variant?: 'default' | 'flat'. The flat variant drops the control shadow and swaps in a soft fill whose border matches the background:

| Token | Description | Light default | |-------|-------------|---------------| | --color-input-flat-bg | Flat-variant input background | #f3f4f6 | | --color-input-flat-border | Flat-variant input border (defaults to the flat background) | var(--color-input-flat-bg) |

Input's prefix (fixed text such as /zaken/ before a slug) renders as a muted segment inside the field, set off by a vertical rule:

| Token | Description | Light default | |-------|-------------|---------------| | --color-input-prefix-bg | Prefix segment background | #f9fafb | | --color-input-prefix-text | Prefix text color | #6b7280 | | --color-input-prefix-border | Rule between prefix and input (defaults to the input border) | var(--color-input-border) | | --color-input-flat-prefix-bg | Prefix segment background, flat variant | rgb(17 24 39 / 0.04) | | --color-input-flat-prefix-border | Rule between prefix and input, flat variant | #e5e7eb |

(InputCheck, InputRadio, InputToggle, and the InputFileMultiple dropzone have no box chrome to flatten, so they don't take the variant.)

Color — input labels, descriptions & errors

| Token | Description | Light default | |-------|-------------|---------------| | --color-input-label-text | Label text color | #111827 | | --color-input-description-text | Description text color | #6b7280 | | --color-input-error-text | Error message text color | #dc2626 |

Color — input icon buttons

| Token | Description | Light default | |-------|-------------|---------------| | --color-input-icon-button-ring | Ring color for icon buttons inside inputs | #e5e7eb | | --color-input-icon-button-icon | Icon color for icon buttons inside inputs | #6b7280 |

Color — file inputs

InputFileSingle and UploadFileTile are composed from existing primitives (input tokens, Button for the inset "Choose" button, ButtonIconSquare for the remove (X) button) — no dedicated color tokens of their own. InputFileMultiple adds:

| Token | Description | Light default | |-------|-------------|---------------| | --color-input-file-icon-bg | Background of the central icon frame inside InputFileMultiple's dropzone | #f3f4f6 |

All three components also rely on the shared --color-status-success / --color-status-error tokens for the green check / red error icons in their upload-state slots.

Color — color input

InputColor reuses the standard input tokens — the color swatch in the field and the outline of the picker's saturation/value plane both derive from --color-input-border, and the field itself uses the same --color-input-* tokens as Input. The picker's hue/brightness gradients and indicator rings are intrinsic to the color-picking UI (not theme-based) and are intentionally not tokenized.

Both halves of InputColor are also exported standalone: ColorPicker (the HSV panel — drop it into your own popover or panel) and ColorSwatch (the small rounded color tile, sized via the size prop or the surrounding ControlSizeContext).

Color — date inputs & calendar

InputDate and InputDateTime are masked text fields with a calendar popover; the field itself uses the --color-input-* tokens and the popover panel the --color-dropdown-* ones.

const [date, setDate] = useState<Date | null>(null);

<InputDate label="Date of birth" value={date} onChange={setDate} />
<InputDateTime label="Hearing" value={date} onChange={setDate} minuteStep={15} min={new Date()} />
<InputDate format="MM/dd/yyyy" locale="en-US" weekStartsOn={0} name="due" />
  • Value is a Date | null in local time (InputDate gives midnight). It is null while the field is empty or holds an incomplete or out-of-range date; value / defaultValue make it controlled or uncontrolled. With a name, a hidden input submits YYYY-MM-DD (or YYYY-MM-DDTHH:mm) like a native date input.
  • format is built from dd, MM, yyyy, HH (24-hour) and mm with any separators; it drives the mask and the placeholder. Defaults: dd-MM-yyyy and dd-MM-yyyy HH:mm.
  • Typing: separators fill themselves in, 4 becomes 04, a typed separator completes the segment (3- → 03-), full segments are overwritten in place, and a pasted ISO date is understood in any format. Leaving the field completes what it can (3-11-25 → 03-11-2025, a missing time → 00:00) and clears what it cannot.
  • Keyboard: ↑ / ↓ step the segment under the caret, Alt+↓ moves into the calendar (arrows, PageUp / PageDown, Home / End, Enter), Esc closes it.
  • locale, weekStartsOn and labels localise the calendar; min / max bound both typing and picking.

Calendar is exported on its own too. When you render it on the server, pass a locale — the default is the runtime's, which can differ between server and browser.

| Token | Description | Light default | |-------|-------------|---------------| | --color-calendar-text | Day, month, year and header text | var(--color-dropdown-item-text) | | --color-calendar-text-muted | Weekday names | #6b7280 | | --color-calendar-text-outside | Days of the previous / next month | #9ca3af | | --color-calendar-text-disabled | Days, months, years and times outside min / max | #d1d5db | | --color-calendar-divider | Rules above "Today" and beside the time columns | var(--color-dropdown-border) | | --color-calendar-cell-bg-hover | Cell background on hover | var(--color-dropdown-item-bg-hover) | | --color-calendar-cell-bg-active | Cell background on press | var(--color-dropdown-item-bg-active) | | --color-calendar-cell-ring | Cell keyboard-focus ring | var(--color-dropdown-item-ring) | | --color-calendar-cell-today-border | Outline marking today | var(--color-button-primary-bg) | | --color-calendar-cell-selected-bg | Selected day / month / year / time | var(--color-button-primary-bg) | | --color-calendar-cell-selected-bg-hover | Selected cell on hover | var(--color-button-primary-bg-hover) | | --color-calendar-cell-selected-bg-active | Selected cell on press | var(--color-button-primary-bg-active) | | --color-calendar-cell-selected-text | Selected cell text | var(--color-button-primary-text) |

Color — select options

| Token | Description | Light default | |-------|-------------|---------------| | --color-option-bg-hover | Option background on hover | #f3f4f6 | | --color-option-bg-active | Option background on press | #e5e7eb | | --color-option-bg-selected | Selected option background | #eff6ff | | --color-option-bg-selected-hover | Selected option background on hover | #dbeafe | | --color-option-bg-selected-active | Selected option background on press | #dbeafe | | --color-option-text-disabled | Disabled option text color | #9ca3af | | --color-input-select-placeholder | Select placeholder text color | #6b7280 |

Color — select search bar

| Token | Description | Light default | |-------|-------------|---------------| | --color-select-search-border | Search input border | #e5e7eb | | --color-select-search-bg | Search input background | rgb(255 255 255 / 0.5) | | --color-select-search-icon | Search icon color | #6b7280 |

Color — toggle

| Token | Description | Light default | |-------|-------------|---------------| | --color-toggle-track-on-bg | Track background when on | #2563eb | | --color-toggle-track-on-border | Track border when on | #2563eb | | --color-toggle-track-off-bg | Track background when off | #d1d5db | | --color-toggle-track-off-border | Track border when off | #d1d5db | | --color-toggle-thumb-bg | Thumb background | #ffffff |

Color — checkbox & radio

| Token | Description | Light default | |-------|-------------|---------------| | --color-check-border | Checkbox/radio border | #d1d5db | | --color-check-ring | Checkbox/radio focus ring | rgb(17 24 39 / 0.1) | | --color-check-checked-bg | Checkbox/radio fill when checked or indeterminate | #2563eb | | --color-check-border-error | Checkbox/radio border when error is set | var(--color-input-border-error) | | --color-check-ring-error | Checkbox/radio hover/focus ring when error is set | var(--color-input-ring-error) | | --color-check-disabled-bg | Checkbox/radio fill when disabled | #f3f4f6 | | --color-check-disabled-border | Checkbox/radio border when disabled | #e5e7eb | | --color-check-disabled-checked-bg | Checkbox/radio fill when disabled and checked/indeterminate | #d1d5db | | --color-check-disabled-label | Checkbox/radio label text when disabled | #9ca3af |

Color — range (slider)

| Token | Description | Light default | |-------|-------------|---------------| | --color-range-track-bg | InputRange track background | #e5e7eb | | --color-range-fill-bg | Filled portion of the track | #2563eb | | --color-range-thumb-bg | Thumb background | #ffffff | | --color-range-thumb-border | Thumb border | #d1d5db | | --color-range-ring | Thumb focus ring | rgb(17 24 39 / 0.1) |

Color — dropdown menu

| Token | Description | Light default | |-------|-------------|---------------| | --color-dropdown-bg | Dropdown panel background | #ffffff | | --color-dropdown-border | Dropdown panel border | #e5e7eb | | --color-dropdown-item-bg-hover | Item background on hover | #f3f4f6 | | --color-dropdown-item-bg-active | Item background on press | #e5e7eb | | --color-dropdown-item-text | Item text color | #111827 | | --color-dropdown-item-ring | Item focus ring | rgb(17 24 39 / 0.1) | | --color-dropdown-group-label | Group label text color | #6b7280 |

Color — tooltip

Defaults mirror the dropdown panel the tooltip historically reused, so an untouched theme looks identical.

| Token | Description | Light default | |-------|-------------|---------------| | --color-tooltip-bg | Tooltip panel background | var(--color-dropdown-bg) | | --color-tooltip-border | Tooltip panel border | var(--color-dropdown-border) | | --color-tooltip-text | Tooltip text color | var(--color-dropdown-item-text) |

Color — tab buttons

| Token | Description | Light default | |-------|-------------|---------------| | --color-tab-text | Tab text and icon color | #111827 | | --color-tab-container-bg | Tab bar background | #f3f4f6 | | --color-tab-bg-hover | Tab background on hover | #e5e7eb | | --color-tab-bg-active | Tab background on press | rgb(209 213 219 / 0.8) | | --color-tab-active-bg | Active tab background | #ffffff | | --color-tab-active-border | Active tab border | #e5e7eb | | --color-tab-count-bg | Count chip background (inactive tab) | #e5e7eb | | --color-tab-count-text | Count chip text (inactive tab) | #374151 | | --color-tab-active-count-bg | Count chip background (active tab) | #111827 | | --color-tab-active-count-text | Count chip text (active tab) | #ffffff |

Color — tabs (underline)

| Token | Description | Light default | |-------|-------------|---------------| | --color-tabs-text | Inactive tab text and icon color | #6b7280 | | --color-tabs-text-hover | Inactive tab text on hover | #374151 | | --color-tabs-active-text | Active tab text and icon color | #111827 | | --color-tabs-border | Bottom rule under the tab list | #e5e7eb | | --color-tabs-active-indicator | Active tab underline | #111827 | | --color-tabs-count-bg | Count chip background | #f3f4f6 | | --color-tabs-count-text | Count chip text | #4b5563 | | --color-tabs-active-count-bg | Count chip background (active tab) | var(--color-tabs-count-bg) | | --color-tabs-active-count-text | Count chip text (active tab) | var(--color-tabs-count-text) |

Color — panel

| Token | Description | Light default | |-------|-------------|---------------| | --color-panel-bg | Panel background | #ffffff | | --color-panel-border | Panel border | #e5e7eb | | --color-panel-text | Panel default text color (inherited by PanelField) | #111827 |

Color — modal & sidebar modal

| Token | Description | Light default | |-------|-------------|---------------| | --color-modal-overlay | Backdrop overlay color | rgb(156 163 175 / 0.3) | | --color-modal-bg | Modal content background | #ffffff |

Color — table

| Token | Description | Light default | |-------|-------------|---------------| | --color-table-header-text | Header text color | #1f2937 | | --color-table-header-bg | Header background | #f9fafb | | --color-table-header-bg-hover | Header background on hover | #f3f4f6 | | --color-table-header-bg-active | Header background on press | #e5e7eb | | --color-table-border | Table border color | #e5e7eb | | --color-table-row-bg | Row background | #ffffff | | --color-table-row-bg-hover | Row background on hover | #f9fafb | | --color-table-row-text | Row text color | #111827 | | --color-table-resize-handle | Column resize handle | #e5e7eb | | --color-table-resize-handle-hover | Resize handle on hover | #d1d5db | | --color-table-resize-handle-active | Resize handle while dragging | #2563eb |

Color — divider

| Token | Description | Light default | |-------|-------------|---------------| | --color-divider | Divider line color | #e5e7eb |

Color — badge

| Token | Description | Light default | |-------|-------------|---------------| | --color-badge-white-bg | White badge background | #ffffff | | --color-badge-white-text | White badge text | #111827 | | --color-badge-white-ring | White badge ring | #d6d3d1 | | --color-badge-black-bg | Black badge background | #000000 | | --color-badge-black-text | Black badge text | #ffffff | | --color-badge-black-ring | Black badge ring | #d6d3d1 |

Colored badges (red, blue, green, etc.) use Tailwind color utility classes and are not token-based. They adapt to dark mode automatically via Tailwind's dark: variants.

Color — status

Shared color tokens for status/notification indicators. Currently used by PanelLink's status prop, but available for any component.

| Token | Description | Light default | |-------|-------------|---------------| | --color-status-error | Error / destructive state | #dc2626 | | --color-status-warning | Warning state | #f59e0b | | --color-status-success | Success state | #16a34a | | --color-status-info | Informational state | #2563eb |

Rich text editor (InputLexical)

InputLexical is a Lexical-powered rich text editor styled like the rest of the kit. It ships with two toolbar variants that share the exact same controls:

  • static (default) — a light toolbar fixed at the top of the editor.
  • floating — a dark bar that appears above the editor while it is focused, matching the editor width.

Lexical and its plugins are peer dependencies — install them alongside the library:

pnpm add lexical @lexical/react @lexical/rich-text @lexical/list @lexical/link @lexical/selection @lexical/utils

Basic usage

The value is a serialized Lexical editor state (a JSON string). Pass the last onChange value back as value to restore content.

import { useState } from "react";
import { InputLexical } from "@matthiaskrijgsman/mat-ui";

function Editor() {
  const [value, setValue] = useState<string>();

  return (
    <InputLexical
      label={"Description"}
      placeholder={"Write something…"}
      toolbar={"floating"}     // or "static" (default), or "none" to hide it
      value={value}
      onChange={setValue}
      autogrow                 // grow with content…
      minRows={4}              // …from a 4-row floor…
      maxRows={12}             // …up to 12 rows, then scroll
    />
  );
}

Sizing mirrors InputTextArea: minRows sets a height floor, maxRows caps the height (content beyond it scrolls), and autogrow lets the editor grow with its content between the two. Without autogrow the editor is fixed at minRows.

Extending the toolbar

The toolbar is assembled from exported building blocks, so you can reorder, drop, or add controls via the renderToolbar slot. It receives { editor, state, tone } and renders into whichever variant is active — the same render function drives both the static and floating bars.

import {
  InputLexical,
  LexicalBlockTypeSelect,
  LexicalFormatButtons,
  LexicalListButtons,
  LexicalLinkButton,
  LexicalHistoryButtons,
  LexicalToolbarDivider,
} from "@matthiaskrijgsman/mat-ui";

<InputLexical
  renderToolbar={() => (
    <>
      <LexicalFormatButtons/>
      <LexicalToolbarDivider/>
      <LexicalListButtons/>
      <LexicalLinkButton/>
      <LexicalToolbarDivider/>
      <LexicalHistoryButtons/>
    </>
  )}
/>;

Building blocks read the active editor and formatting state/tone from context, so they work in either variant with no extra wiring. Dividers automatically flip orientation (and the whole toolbar collapses overflowing controls into a vertical ⋮ dropdown) when space runs out — return each control as a top-level child so it stays individually measurable.

Second row & non-collapsible toolbars

Two more layout knobs on InputLexical (and on LexicalFloatingToolbar / FloatingToolbarShell as renderSecondRow / secondRow and collapsible):

  • renderToolbarSecondRow — a second row of building blocks for the floating toolbar, rendered below a divider (LexicalToolbarRowDivider).
  • toolbarCollapsible={false} — disables the ⋮ overflow dropdown; controls that no longer fit wrap onto extra rows instead, each separated by a divider. Works for both the static and floating toolbar.
<InputLexical
  toolbar={"floating"}
  toolbarCollapsible={false}
  renderToolbarSecondRow={() => (
    <>
      <LexicalAlignButtons/>
      <LexicalToolbarDivider/>
      <LexicalHistoryButtons/>
    </>
  )}
/>;

Custom controls

Build your own control with LexicalToolbarButton plus Lexical's editor context. useLexicalToolbar() exposes the current { state, tone }:

import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { FORMAT_TEXT_COMMAND } from "lexical";
import { IconStrikethrough } from "@tabler/icons-react";
import { LexicalToolbarButton, useLexicalToolbar } from "@matthiaskrijgsman/mat-ui";

const StrikethroughButton = () => {
  const [editor] = useLexicalComposerContext();
  const { tone } = useLexicalToolbar();
  return (
    <LexicalToolbarButton
      Icon={IconStrikethrough}
      tone={tone}
      aria-label={"Strikethrough"}
      onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")}
    />
  );
};

Registering extra Lexical nodes

The built-in set covers headings, lists, links, and quotes. For anything else (tables, mentions, code blocks, …) install the node's package yourself and pass the node via the nodes prop — it is registered alongside the built-in set:

pnpm add @lexical/code
import { CodeNode } from "@lexical/code";

<InputLexical nodes={[CodeNode]} renderToolbar={/* … */} />;

mat-ui does not bundle lexical or any @lexical/* package — they are peer dependencies, so a single shared copy is used. Only register a given node type once: don't pass a node that is already in the built-in set, or Lexical throws a duplicate-type error.

Adding plugins (the children slot)

A Lexical feature is usually a node plus a plugin. Register the node with nodes, then mount the plugin(s) as children — they run inside the editor alongside the built-ins (history, lists, links). Any @lexical/react plugin or your own works:

import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
import { CodeHighlightPlugin } from "@lexical/react/LexicalCodeHighlightPlugin";

<InputLexical nodes={[CodeNode]}>
  <CodeHighlightPlugin />
  <TabIndentationPlugin />
  {/* …or your own plugin using useLexicalComposerContext() */}
</InputLexical>;

Style custom nodes by merging theme classes over the defaults with the theme prop:

<InputLexical nodes={[CodeNode]} theme={{ code: "my-code-block" }}>
  <CodeHighlightPlugin />
</InputLexical>;