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

kob-ui

v0.4.1

Published

Dependency-free UI widget library for classic data-entry web apps — datagrid, dialog, combobox, datebox, tabs, layout and more. A free-for-commercial-use alternative to jQuery EasyUI.

Readme

kob-ui

Dependency-free UI widgets for the kind of web app that is mostly forms and tables — a datagrid that filters and sorts without a round trip, draggable windows, a combobox, a date picker, tabs, a layout frame. No jQuery, no build step required.

It grew out of replacing jQuery EasyUI in a production app, so the shapes will look familiar if you have used EasyUI — but the code is new and the API is plain JavaScript.

  • Free for commercial use. No fee, no licence key, no attribution in your UI, no limit on projects, clients, servers or domains.
  • One JS file and one CSS file, zero runtime dependencies
  • Works from a <script> tag or as an ES module, with TypeScript types
  • Themed entirely through CSS custom properties: light, dark and black
  • English and Thai out of the box, including the Buddhist calendar

This is not an open-source project. The source is not published, and the library may not be modified, forked or republished. You may use it freely, including commercially, under the licence — see Licence below.

Website, docs and live examples → — every widget running in the page, with the source that produced it.

Writing this code with an AI assistant?

Point it at the plain-text reference that ships in this package, and it will have every option, method, event and example without guessing:

| File | What it is | | --- | --- | | node_modules/kob-ui/llms.txt | The short version: install, the rules that decide whether generated code works, and a link per widget. Also at https://kob-ui.com/llms.txt. | | node_modules/kob-ui/llms-full.txt | The whole documentation site as one file — every option table, every worked example, the icon list and the TypeScript declarations. Also at https://kob-ui.com/llms-full.txt. |

Worth doing: dist/ is obfuscated, so an assistant that tries to work the API out by reading the bundle — its usual fallback — will find nothing to read. These two files are what it should read instead.


Install

npm i kob-ui

Or straight from a CDN, no install at all:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/kob-ui@latest/dist/kob-ui.min.css">
<script src="https://cdn.jsdelivr.net/npm/kob-ui@latest/dist/kob-ui.min.js"></script>

Quick start

From a script tag

<link rel="stylesheet" href="node_modules/kob-ui/dist/kob-ui.css">
<script src="node_modules/kob-ui/dist/kob-ui.min.js"></script>

<div id="grid" style="height:400px"></div>

<script>
  Kob.datagrid('#grid', {
    columns: [
      { field: 'room',   title: 'Room',   width: 100 },
      { field: 'tenant', title: 'Tenant', width: 180 },
      { field: 'rent',   title: 'Rent',   width: 110, align: 'right' }
    ],
    url: '/api/rooms'
  });
</script>

As a module

import Kob from 'kob-ui';
import 'kob-ui/css';

const grid = Kob.datagrid('#grid', { columns, data: rows });
grid.sort('rent', 'desc');

Inside React, Vue or Angular

kob-ui builds DOM imperatively, so it works in any framework on one condition: give it an element the framework leaves alone, and destroy the widget when that element goes away. destroy() takes back every node the widget added, so remounting on the same element — React StrictMode, a revisited route — produces exactly one widget.

function OrderGrid() {
  const host = useRef(null);

  useEffect(() => {
    const grid = Kob.datagrid(host.current, { columns, url: '/api/rooms' });
    return () => grid.destroy();
  }, []);

  return <div ref={host} style={{ height: 420 }} />;
}

From markup

Give an element the widget's class and it is built for you:

<input class="kob-textbox"  data-options="label:'Name',required:true">
<input class="kob-datebox"  data-options="label:'Move in'">
<a class="kob-linkbutton icon-save"><span class="kob-icon"></span>Save</a>

<script>Kob.parse(document);</script>

data-options takes a compact key:value list — strings in quotes, plus numbers, true, false and null. Anything richer (functions, arrays of column definitions) is passed from JavaScript.

Every accessor returns the same instance for the same element, so you can reach a widget again later without keeping a reference:

Kob.datagrid('#grid').reload();

Widgets

| Widget | Class | What it does | | --- | --- | --- | | Kob.textbox | kob-textbox | Text input with an optional label | | Kob.passwordbox | kob-passwordbox | Same, masked | | Kob.numberbox | kob-numberbox | Min/max clamping, precision, thousands separator | | Kob.combobox | kob-combobox | Value/text list, local or from a URL, keyboard navigation | | Kob.datebox | kob-datebox | Month calendar; reads and writes ISO dates | | Kob.checkbox | kob-checkbox | A real checkbox drawn as a switch | | Kob.timebox | kob-timebox | A time of day, picked off a list or typed forgivingly | | Kob.datetimebox | kob-datetimebox | The calendar with a time strip under it | | Kob.daterangebox | kob-daterangebox | From one date to another, with the shortcuts a report needs | | Kob.radiobox | kob-radiobox | One choice out of several, as a group | | Kob.searchbox | kob-searchbox | Search field with a clear button and a debounced onSearch | | Kob.filebox | kob-filebox | Drop zone, file list, and upload with a progress bar | | Kob.linkbutton | kob-linkbutton | Toolbar button with a masked-SVG icon | | Kob.panel | kob-panel | Titled box, collapsible, can load HTML from a URL | | Kob.layout | kob-layout | north / west / center / east / south with draggable splitters | | Kob.tabs | kob-tabs | Tab pages from child elements, closable and addable | | Kob.dialog | kob-dialog | Modal dialog or a draggable, resizable window | | Kob.datagrid | kob-datagrid | Sort, filter, search, page, inline edit | | Kob.treegrid | kob-treegrid | The datagrid, with rows that have children | | Kob.propertygrid | kob-propertygrid | Name and value, grouped and editable — the settings sheet | | Kob.calendar | kob-calendar | The month grid outside a dropdown | | Kob.slider | kob-slider | One value or a span of two, with ticks and labels | | Kob.accordion | kob-accordion | Titled sections that take turns | | Kob.menubutton | kob-menubutton | A button whose job is to open a menu | | Kob.splitbutton | kob-splitbutton | A default action, with the alternatives behind a caret | | Kob.transfer | kob-transfer | Two lists and the buttons between them | | Kob.tooltip | kob-tooltip | The sentence a control cannot fit on itself | | Kob.timeline | kob-timeline | Who did what, and when | | Kob.form | kob-form | Read, fill, validate and clear a whole form | | Kob.sidebar | kob-sidebar | The menu down the side: groups, a fold to a rail, a drawer on a phone | | Kob.menu | kob-menu | Submenus, shortcuts, separators; binds to right-click | | Kob.menubar | kob-menubar | The File / Edit / View strip | | Kob.ribbon | kob-ribbon | Tabs of grouped commands, folds away | | Kob.tree | kob-tree | Expand, select, tri-state checkboxes, lazy children | | Kob.desktop | kob-desktop | Wallpaper icons, taskbar, start menu, app windows | | Kob.filterbutton | kob-filterbutton | The grid filter chip, standalone | | Kob.progressbar | kob-progressbar | Measured or indeterminate, striped | | Kob.preloader | kob-preloader | Busy mask or page splash, plus .kob-spinner | | Kob.alertbox | kob-alert | The inline banner (Kob.alert is the modal) | | Kob.badge | kob-badge | Counts that cap and hide themselves |

Plus Kob.alert, Kob.confirm, Kob.prompt and Kob.toast for messages, Kob.sweet for the big centred confirmation, Kob.modal / Kob.window as the two spellings of dialog, and CSS-only kob-card, kob-statusbar, kob-btn-group, kob-toolbar, a 12-column kob-row and the kob-flex helpers.


datagrid

The grid holds the full result set and does searching, filtering, sorting and paging locally, so changing a filter never costs another request.

const grid = Kob.datagrid('#grid', {
  columns: [
    { field: 'room',   title: 'Room',   width: 100 },
    { field: 'rent',   title: 'Rent',   width: 110, align: 'right',
      formatter: v => Number(v).toLocaleString(),
      editor: { type: 'numberbox', options: { precision: 2 } } },
    { field: 'status', title: 'Status', width: 110 }
  ],
  url: '/api/invoices',
  rownumbers: true,
  pageSize: 20,
  onDblClickRow: i => grid.beginEdit(i),
  onEndEdit: (i, row) => save(row)
});

Filter chips. Above the header, one chip per column. The chip decides what it offers by looking at the data: a pick-list for categories, a lower bound for numbers, a contains box for free text. Turn them off per column with filter: false, force a kind with filter: 'text', or hide the row entirely with filterable: false.

Filters read what the user sees. A column with a formatter is flattened to text once per row and cached, so a status column rendering 1 as a green "Paid" badge filters and searches as Paid.

Useful methods: loadData, reload, sort, setFilter, clearFilters, search, gotoPage, getSelected, getSelections, beginEdit, endEdit, resize.

Selecting more than one. checkbox: true adds a column of them and turns multi-select on; Ctrl (Cmd) toggles a row, Shift takes the run from the last row touched, and the header box ticks the page in front of you. The selection is kept as rows rather than as highlighted <tr>s, so it survives paging, sorting and filtering — getSelections() returns rows picked three pages ago.

One column, and it stops looking like a table. A single-column grid has nothing to sort against, resize or filter by, so it drops the header furniture and the chip row: what is left is a paged, searchable list whose rows look like whatever the formatter returns. showHeader: false does the same to a grid of many columns.

Totals along the bottom. A column with a summarysum, avg, count, min, max, a label, or a function — gets a value in a row under the body. The totals are taken over what the filters left rather than over the page on screen, and a sum is shown through the column own formatter so it reads in the same units the column does.

Frozen columns. frozen: true on a leading column pins it while the rest scrolls sideways, which a table of twenty columns cannot do without: a row you cannot identify is a row you cannot read. Only a leading run can be frozen.

columns: [
  { field: 'room', title: 'Room', width: 90, frozen: true, summary: 'Total' },
  { field: 'rent', title: 'Rent', width: 120, formatter: money, summary: 'sum' }
]

Rows with children. Kob.treegrid is the same widget with a caret in one column: the tree is flattened into the rows the grid already sorts, filters, pages and selects, so the chips, the search box, the totals row and inline editing all go on working. Sorting orders siblings inside their parent, and searching keeps the ancestors of a match — a row three levels down does not say where it is on its own.

A row that opens: master and detail. An invoice is a header and a list of lines, and the list belongs under the header rather than beside it. detail is a function handed the row and an empty <div> just placed under it; what you put in the div is yours, and most often it is a second datagrid.

Kob.datagrid('#invoices', {
  columns: [ /* … */ ],
  data: invoices,
  detailHeight: 240,
  detail: (row, panel) => Kob.datagrid(panel, {
    columns: lineColumns,
    url: `/api/invoices/${row.id}/lines`,
    pagination: false,
    fit: true
  })
});

It runs once per row, the first time that row is opened, and the panel is then kept: sorting, filtering or turning a page moves the same element into the new table instead of asking your server for the same lines again. Closing the panel throws it away and destroys everything KOB built inside it.

Two grids sharing one <tbody> is the part that usually goes wrong, so it is worth saying plainly: every row knows which grid drew it. A click on a line does not select the invoice behind it, the outer grid's getSelections() never returns a line, and the inner grid's selection stays its own.

This is not the treegrid, and the choice between them is not a matter of taste. A treegrid's children are more rows of the same columns — the same table, indented. A detail panel is a different table, with its own columns, totals and URL. A folder of files is the first shape; an invoice and its lines is the second.

A button that offers alternatives. Kob.menubutton is a button whose only job is to open a menu. Kob.splitbutton has a default action and a menu — the Save / Save-and-print shape that every record screen ends up needing:

Kob.splitbutton('#save', {
  items: [{ text: 'Save and print' }, { text: 'Save and close' }],
  onClick: save,                       // the button half
  onMenuClick: item => runVariant(item) // the caret half
});

The caret is a real <button> with its own focus stop, so Tab reaches it and opens the menu. That is not decoration: a caret only a mouse can reach hides every alternative from a keyboard user, and in this shape the alternatives are usually the destructive ones.

Sections that take turns. Kob.accordion reads your markup — every direct child is a section, titled by its title attribute or by a heading at the top of the block:

<div class="kob-accordion">
  <div title="Company">   … </div>
  <div><h3>Billing</h3> … </div>
</div>

With one section open at a time, clicking the open header does nothing by default rather than collapsing the box to a column of empty bars. Set collapsible: true when the sections really are all optional.

A number, or a span of two. Kob.slider takes min, max, step, marks and range: true for two handles that cannot cross. Each handle is a real <input type="range"> with the track painted over it, so arrow keys, Home, End and touch behaviour come from the browser rather than from a pointermove handler that forgot about them. Hang your work off onChange, which fires when the handle is let go — onSlide fires dozens of times per drag.

Up and down on a number field. Kob.numberbox takes spinners: true and a step. The arrow keys step it either way, with or without the buttons.

The month on its own. Kob.calendar is the grid outside a dropdown, for when the date is the screen — a booking sheet, a leave request, a day view other panels follow. It draws with the same code datebox uses, so the Buddhist era, the first day of the week and the min/max clamp behave identically in both.

Name and value, down the page. Kob.propertygrid is the settings sheet: two columns, group headings, editable in place. It is a datagrid with the columns decided, which means the search box and inline editing arrive already working — and a settings page with sixty entries gets a search box for free.

Kob.propertygrid('#settings', {
  editable: true,
  rows: [
    { name: 'Currency', value: 'THB', group: 'Billing' },
    { name: 'VAT rate', value: 7,     group: 'Billing', editor: 'numberbox' }
  ],
  onValueChange: (row, value) => save(row.name, value)
});

Editing without an actions column. Not every table has room for a column of buttons. rowActions puts them over the row instead — a strip that follows the pointer and parks on the row the user clicked, so it stays reachable from the keyboard:

Kob.datagrid('#grid', {
  columns: [ /* … */ ],
  data: rows,
  rowActions: [
    { text: 'Edit', iconCls: 'icon-edit', handler: edit },
    { text: 'Delete', iconCls: 'icon-remove', color: 'danger',
      disabled: row => row.status === 'paid', handler: remove }
  ],
  onDblClickRow: (i, row) => edit(row)
});

The strip covers the right-hand end of the row while it is open, so put the column you can least afford to hide on the left. That is the trade for not spending a column on buttons.


dialog

const win = Kob.dialog('#editor', {
  title: 'Edit invoice',
  width: 640,
  modal: false,            // draggable, resizable, minimise + maximise
  buttons: [
    { text: 'Save',   iconCls: 'icon-save', primary: true, handler: save },
    { text: 'Cancel', iconCls: 'icon-cancel', handler: d => d.close() }
  ]
});
win.open();

modal: true (the default) dims the page and shows only a close button. sheet: true docks the window to the bottom edge, which is the shape phones expect.

sidebar

The menu down the side of an application, and the three pieces of state it always needs: which entry is current, whether it is folded to a rail, and what it does when the screen is too narrow to hold it.

const nav = Kob.sidebar('#nav', {
  title: { text: 'RentRoll', iconCls: 'icon-building' },
  items: [
    { title: 'Overview' },                       // a heading, not an entry
    { text: 'Dashboard', iconCls: 'icon-dashboard', name: 'dashboard' },
    { text: 'Invoices',  iconCls: 'icon-invoice', name: 'invoices',
      badge: 28, badgeColor: 'danger' },
    { text: 'Buildings', iconCls: 'icon-building', items: [   // one fold deep
      { text: 'Sukhumvit', name: 'b1' },
      { text: 'Thonglor',  name: 'b2' }
    ] },
    { spacer: true },                             // the rest sits at the bottom
    { text: 'Sign out', iconCls: 'icon-logout', name: 'signout' }
  ],
  active: 'dashboard',
  onSelect: item => router.go(item.name)
});

// One call for the button in the bar, whatever the screen is doing.
document.querySelector('#menu-btn').onclick = () => nav.toggle();

It folds one way on a desktop and another on a phone. toggle() collapses the column to a rail of icons where there is room, and below breakpoint (720px) opens and closes a drawer instead — because a rail of unlabelled icons is not a menu anyone can read on a phone. The drawer takes the column out of the layout, dims the page behind it, closes on Escape or on choosing somewhere to go, and hands focus back to the button that opened it.

It works on the markup you already have. Leave items out and it adopts the kob-sidebar-item links already in the element: your click handlers keep working, select() and badge() can reach the entries, and destroy() gives the markup back as it was found. Entries it builds are a <button> unless you give them an href, and the current one is marked aria-current="page" — an <a> with no href is not focusable and is announced as nothing, which is the shape most hand-written sidebars have.

Useful methods: select, getSelected, setItems, badge, enable, disable, collapse, expand, toggle, open, close, expandGroup.

Validation

required answers one question about a field. Everything else — an address that looks like one, a code in a format, a quantity inside a bound, a password typed twice — is a rules option away, on every field widget there is.

Kob.textbox('#email', { label: 'Email', rules: 'email' });
Kob.textbox('#name',  { required: true, rules: [{ minLength: 3 }, { maxLength: 60 }] });
Kob.numberbox('#qty', { rules: [{ integer: true }, { min: 1 }, { max: 36 }] });

// equalTo reads the other field through its widget, so a numberbox compares
// numbers and a datebox compares ISO dates.
Kob.passwordbox('#again', { rules: { equalTo: '#password' }, message: 'They do not match' });

// A rule of your own, once, anywhere.
Kob.addRule('sku', v => /^[A-Z]{2}-d{4}$/.test(v), 'Two letters, a dash, four digits');

Named rules: email, url, number, integer, digits, minLength, maxLength, length, min, max, pattern, equalTo. A rule can also be a function returning true or the message to show. An empty field passes every rule but required — otherwise "optional, but an email address if it is filled in" could not be said. A rule name nobody registered throws rather than quietly passing.

Each rule carries its own sentence from the locale bundle, so Kob.locale('th') translates the failures along with everything else; message on the field overrides whichever rule failed. The message is written under the whole field and the control gets aria-invalid and an aria-describedby pointing at it — a red border says that something is wrong, never what.

A field with rules checks itself as the user leaves it (validateOn: 'blur'); form.validate() checks all of them and focuses the first that is wrong. A field with no rules behaves exactly as it did before, so nothing changes in a form until you give one a rule.


Messages

Every message helper returns a promise, and still calls the old-style fn callback if you pass one:

if (await Kob.confirm({ title: 'Delete', msg: 'Remove this tenant?' })) {
  await api.remove(id);
  Kob.toast({ msg: 'Deleted', type: 'success' });
}

Remote data

combobox and datagrid fetch through one function you can replace, so the widgets never need to know about your backend:

Kob.config.request = async (url, params) => {
  const res = await fetch(url + new URLSearchParams(params ?? {}), {
    headers: { Authorization: 'Bearer ' + token }
  });
  if (res.status === 401) { location.href = '/login'; return []; }
  const payload = await res.json();
  if (!payload.success) throw new Error(payload.message);
  return payload.data;            // an array, or { rows, total }
};

Per-widget reshaping is still available through loadFilter.


Theming

Every colour, radius and shadow is a CSS custom property. Override them on :root and the whole library follows:

:root {
  --kob-accent: #6d3bd4;
  --kob-accent-dark: #4c2597;
  --kob-radius: 14px;
}

Three themes ship: light, dark, and black — near-black neutral greys with no colour cast, for people who want a dark screen without the blue. Leave the attribute off and the library follows the OS, choosing between light and dark; set it and that choice is pinned.

<html data-kob-theme="black">   <!-- or "dark", or "light" -->

Switching at runtime is one attribute; remembering the choice is yours, as the library never writes to storage.

var THEMES = ['light', 'dark', 'black'];
var here = document.documentElement.getAttribute('data-kob-theme') || 'light';
document.documentElement.setAttribute('data-kob-theme',
  THEMES[(THEMES.indexOf(here) + 1) % THEMES.length]);

A fourth theme is a block of your own — only the tokens that differ need redefining:

:root[data-kob-theme="sepia"] {
  --kob-bg: #f4ecdf;
  --kob-surface: #fbf6ec;
  --kob-text: #3b3026;
}

Scrollbars are themed too — every box the library scrolls, plus anything inside a window or panel. Add class="kob-scrollbars" to <html> to carry that to the rest of the page.

dist/kob-ui.css ships unminified and declares every token at the top, so the full list is readable straight from the file you installed. The theming reference groups them by what they affect.

Button colours

Five colours in two weights — solid for the one action a screen is really about, soft for the secondary ones beside it:

<a class="kob-linkbutton kob-linkbutton-danger">Delete</a>
<a class="kob-linkbutton kob-linkbutton-danger kob-linkbutton-soft">Delete</a>
<button class="kob-btn kob-btn-success">Approve</button>
Kob.linkbutton('#save', { text: 'Save', iconCls: 'icon-save', color: 'accent' });
Kob.linkbutton('#del',  { text: 'Delete', color: 'danger', soft: true });

color takes accent, success, danger, warn or info, and primary: true is the older spelling of color: 'accent'. Each colour is a trio of tokens — --kob-danger-solid for a solid fill, --kob-on-danger for its label, --kob-danger-text for the label of a soft button — because the plain --kob-danger is tuned to read as coloured text on the page and fails contrast in the other two roles. Every one clears 4.5:1 in all three themes.

Button shapes, and toolbars

iconAlign puts the icon on any of the four sides, and size takes sm and lg with the middle size as the default. Stacked and large together is the button a desktop toolbar has been made of for thirty years — a large glyph with its name underneath — and there the extra size goes into the glyph rather than the label, because a toolbar button is found by its picture and confirmed by its name.

Kob.linkbutton('#new', { text: 'New', iconCls: 'icon-add', iconAlign: 'top', size: 'lg' });
Kob.linkbutton('#reload', { iconCls: 'icon-reload', title: 'Reload' });   // no caption
<!-- the same, as markup -->
<div class="kob-toolbar">
  <a class="kob-linkbutton kob-icon-top kob-linkbutton-lg icon-add">
    <span class="kob-icon"></span>New
  </a>
  <span class="kob-sep"></span>          <!-- divides one group from the next -->
  <a class="kob-linkbutton kob-icon-top kob-linkbutton-lg icon-print">
    <span class="kob-icon"></span>Print
  </a>
  <span class="kob-spacer"></span>       <!-- pushes the rest to the far end -->
  <a class="kob-linkbutton kob-icon-only icon-settings" title="Settings">
    <span class="kob-icon"></span>
  </a>
</div>

| Class | Option | | | --- | --- | --- | | kob-icon-right | iconAlign: 'right' | Icon after the caption | | kob-icon-top / kob-icon-bottom | iconAlign: 'top' | Stacked into a column | | kob-linkbutton-sm / -lg | size: 'sm' | 14px or 20px glyph — 24px once stacked | | kob-icon-only | — | Squares up a button with no caption |

There is no toolbar widget, and none is needed. kob-toolbar is a flex row with a gap; kob-sep divides groups and kob-spacer pushes what follows to the far end. What goes in it are ordinary buttons, so the behaviour a toolbar actually needs — Delete greyed out until a row is selected — is enable() and disable() on the button itself. If you want named groups under the strip as well, that is ribbon.


Icons

Icons are single-colour SVGs applied as a CSS mask, so they inherit the element's color and ship inside the stylesheet — no image requests.

<a class="kob-linkbutton icon-add"><span class="kob-icon"></span>Add</a>
Kob.linkbutton('#btn', { iconCls: 'icon-add', text: 'Add' });

181 icons are included. Your own artwork works the same way without touching the library — point the mask at your own SVG:

.icon-invoice .kob-icon {
  -webkit-mask-image: url(/img/invoice.svg);
          mask-image: url(/img/invoice.svg);
}

Localisation

Kob.locale('th');     // Thai, with Buddhist-era years in the datebox

Kob.addLocale('de', {
  ok: 'OK', cancel: 'Abbrechen', today: 'Heute',
  months: ['Januar', 'Februar' /* … */],
  weekdaysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
  dateFormat: 'dd.mm.yyyy'
});
Kob.locale('de');

Missing keys fall back to English. A datebox always reads and writes ISO yyyy-mm-dd through getValue() / setValue(), whatever the locale shows on screen — so the calendar in use never changes what you send to the server.


Writing your own widget

Extending kob-ui from the outside is expressly permitted by the licence. The Widget base class and register() are public API:

import { Widget, register } from 'kob-ui';

class Rating extends Widget {
  static widgetName = 'rating';
  static defaults = { max: 5, value: 0, onChange: null };

  init() {
    this.render();
    this.track(Kob.dom.on(this.el, 'click', 'span', (e, star) => {
      this.setValue(Number(star.dataset.n));
    }));
  }
  setValue(v) { this.options.value = v; this.render(); this.emit('onChange', v); return this; }
  render() { /* … */ }
}

export const rating = register(Rating);   // Kob.parse now builds class="kob-rating"

track() takes the unbind function returned by Kob.dom.on, so destroy() cleans up on its own. What you write this way is your own work — it is not a derivative of kob-ui, and the licence does not reach it.


Browser support

Chrome/Edge 80+, Firefox 78+, Safari 14+. No polyfills needed.

Bugs and requests

kob-ui is developed by one author and the source is not published, so patches cannot be accepted and there is no public issue tracker. The library is provided as-is; fixes and improvements ship in official releases on npm at the author's discretion. Pin a version you have tested and upgrade deliberately.

Licence

kob-ui is proprietary software, free of charge. In short:

| | | | --- | --- | | ✅ Use it in commercial and paid products | no fee, no key, no attribution needed | | ✅ Unlimited projects, clients, servers, domains | | | ✅ Ship it inside your app or website | bundling and minifying are explicitly allowed | | ✅ Extend it through Widget / register and CSS variables | your code stays yours | | ❌ Modify, patch or fork the library itself | | | ❌ Reverse engineer or deobfuscate it | | | ❌ Republish it as a library, package or CDN copy | |

This table is a summary, not the licence. LICENSE is the licence, and it governs.

Copyright © 2026 kob. All rights reserved.