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.
Maintainers
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-uiOr 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 summary — sum, 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.
