Installation

kob-ui ships one JavaScript file and one stylesheet. There is nothing to install alongside it and no build step unless you want one.

$npm i kob-ui

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> <script> Kob.datagrid('#grid', { columns, data }); </script>

Bundler

import Kob from 'kob-ui'; import 'kob-ui/css'; // Named imports work too, and tree-shake in an ESM build. import { datagrid, dialog, toast } from 'kob-ui';

What is in the package

FileFormatUse it for
dist/kob-ui.min.jsIIFEA plain <script> tag. Defines window.Kob.
dist/kob-ui.jsIIFEThe same, unminified, for debugging.
dist/kob-ui.esm.jsESMimport in a bundler.
dist/kob-ui.cjsCommonJSrequire().
dist/kob-ui.cssCSSThe whole stylesheet, icons included.
types/index.d.tsTypeScriptPicked up automatically.

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

Creating widgets

Every widget has one accessor. Call it with options to build the widget; call it again with nothing to get the same instance back.

// Build it. The target may be a selector string or an element. const grid = Kob.datagrid('#orders', { columns, url: '/api/orders' }); // Anywhere else, later — same object, no bookkeeping. Kob.datagrid('#orders').reload(); // Passing options to an existing widget merges them and refreshes it. Kob.datagrid('#orders', { pageSize: 100 }); // Or keep it explicit. Kob.instance(document.querySelector('#orders'), 'datagrid');

Methods return the instance where there is nothing else to return, so they chain: grid.search('bangkok').gotoPage(1).

Events

Callbacks are options, named onSomething. Inside one, this is the widget's element.

Kob.datagrid('#orders', { onClickRow: (index, row) => select(row), onLoadSuccess: function () { console.log(this.id, 'loaded'); } });

Cleanup

destroy() unbinds listeners and puts the DOM back roughly as it was. Kob.unparse(root) does it for everything under an element — worth calling before you tear a screen out of the page.

Markup and data-options

A widget can also be declared in HTML. Kob.parse(root) walks the markup and builds anything whose class names a widget.

<input class="kob-textbox" data-options="label:'Name',labelWidth:110,required:true"> <div class="kob-panel" data-options="title:'Summary',collapsible:true">…</div>
Kob.parse(document); // build everything on the page Kob.parse(myFragment); // …or just what you injected Kob.autoParse(); // build on DOMContentLoaded

Parsing twice is safe: an element that already carries a widget is skipped, so you can call it again after injecting HTML.

The two can be combined. Declare the layout in markup, then hand the real configuration over from JavaScript — the widget merges it and rebuilds. That is how a datagrid declared as class="kob-datagrid" picks up its columns later.

What data-options accepts

Quoted strings, numbers, true, false and null. Functions, formatters and column arrays have no sensible text form, so pass those from JavaScript.

Loading remote data

combobox and datagrid both take a url. Everything they fetch goes through one function, so authentication and response shapes are configured once for the whole library.

Kob.config.request = async (url, params) => { const query = new URLSearchParams(params ?? {}); const res = await fetch(`${url}?${query}`, { 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 } }; // Called whenever the above rejects. Kob.config.onRequestError = (err, url) => reportToSentry(err, url);

A single widget can still reshape its own payload with loadFilter.

Theming

Nothing in the stylesheet hard-codes a colour. Redefine the tokens on :root — or on any container — and everything inside follows.

:root { --kob-accent: #6d3bd4; --kob-accent-dark: #4c2597; --kob-accent-soft: rgba(109, 59, 212, 0.09); --kob-radius: 14px; }
TokenUsed for
--kob-accentPrimary buttons, selection, focus rings, active tabs
--kob-accent-darkHeader text, sorted column, emphasis on the accent
--kob-accent-softHeaders, hovers, striped rows, chip backgrounds
--kob-accent-hoverThe stronger hover state
--kob-surface / --kob-surface-2Panels, windows, dropdowns
--kob-bgThe ground the widgets sit on
--kob-text / --kob-mutedBody text, secondary text
--kob-border / --kob-border-softOutlines, table rules
--kob-danger / --kob-success / --kob-warn / --kob-infoStatus colours and badges
--kob-radius / --kob-radius-sm / --kob-radius-xsCorner rounding
--kob-shadow-1 / -2 / -3Cards, dropdowns, windows
--kob-scrollbar-thumb / -hover / -track / -sizeScrollbars — see below

Scrollbars

The browser's own scrollbars are a fixed grey that reads as a foreign object on a dark surface, so every box the library scrolls gets one painted from the tokens instead — vertical, horizontal, and the corner where the two meet. Anything scrolling inside a window, panel or layout matches too.

To carry the same treatment to the rest of the page, put the class on the root element:

<html class="kob-scrollbars">

A region that keeps its own colours — a dark code panel on a light page, say — just redefines the tokens locally:

.code { --kob-scrollbar-thumb: rgba(207, 228, 236, 0.26); --kob-scrollbar-thumb-hover: rgba(207, 228, 236, 0.45); }

The three themes

Three palettes ship. Leave the attribute off and the library follows the operating system, choosing between light and dark; set it and that choice is pinned.

ValueWhat it looks like
lightThe default. Cool greys on white.
darkBlue-grey night theme, surfaces from #0b1e27 up.
black Neutral near-black with no colour cast — surfaces from #0f1011, and the header and hover washes are white at low alpha rather than a tinted accent, so nothing goes blue. Deliberately not #000: pure black under near-white text is the highest contrast a panel can make, which reads as glare in a dim room.
<html data-kob-theme="black"> <!-- or "dark", or "light" -->

Switching at runtime is one attribute. Remembering the choice is on you — the library never writes to storage:

var THEMES = ['light', 'dark', 'black']; function nextTheme() { var here = document.documentElement.getAttribute('data-kob-theme') || 'light'; var next = THEMES[(THEMES.indexOf(here) + 1) % THEMES.length]; document.documentElement.setAttribute('data-kob-theme', next); localStorage.setItem('theme', next); } // Restore before the first paint, or the page flashes the wrong theme. var saved = localStorage.getItem('theme'); if (saved) { document.documentElement.setAttribute('data-kob-theme', saved); }

A fourth theme is a block of your own. Only the tokens that differ need redefining — everything else falls through to the light values:

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

Localisation

Every string the widgets show, plus the date format and calendar era, comes from the active locale. English and Thai are built in.

Kob.locale('th'); // switch Kob.locale(); // read the current one Kob.t('showing', { from: 1, to: 20, total: 93 }); Kob.addLocale('de', { ok: 'OK', cancel: 'Abbrechen', today: 'Heute', months: ['Januar', 'Februar', /* … */], weekdaysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], firstDay: 1, dateFormat: 'dd.mm.yyyy' }); Kob.locale('de'); // gaps fall back to English

Dates stay ISO. A Thai locale shows 09/03/2567 on screen, but datebox.getValue() still returns '2024-03-09'. The calendar in use never changes what you send to the server.

The datebox example further down switches between English and Thai live, and reports both what is on screen and what getValue() returns.

Icons

181 single-colour SVGs applied as a CSS mask. They take the element's color, live inside the stylesheet, and cost no requests.

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

The full set — click any icon to copy its class name:

Your own artwork works the same way, without touching the library — point the mask at your SVG:

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

textbox, numberbox, passwordbox

Three thin wrappers over <input>: an optional label, a required flag, and for numbers, clamping and formatting.

Options

OptionTypeDefaultDescription
labelstring''Draws a label next to the field. Omit for a bare input.
labelWidthnumber110Label width in pixels when it sits to the left.
labelPosition'left' | 'top'autoDefaults to top on touch screens, left otherwise.
promptstring''Placeholder text.
valueanyInitial value.
requiredbooleanfalseChecked by isValid() and by the containing form.
readonly / disabledbooleanApplied to the underlying input.
min / maxnumbernumberbox: clamped on blur.
precisionnumbernumberbox: decimal places applied on blur.
groupSeparatorstring''numberbox: thousands separator shown while unfocused.
onChangefunction(value)Fires on the input's native change.

Methods

MethodReturnsDescription
getValue()string | numbernumberbox returns a number, or '' when empty.
setValue(v)thisnumberbox also clamps and formats.
clear()thisEmpties the field and drops the invalid mark.
isValid()booleanWhether required is satisfied.
enable() / disable()this 
readonly(mode)this 
focus()this 

combobox

A text input backed by a list. The value the form submits and the text the user reads are kept apart, so an id column never leaks onto screen.

Options

OptionTypeDefaultDescription
dataobject[]nullRows to show.
urlstring''Fetched through Kob.config.request.
queryParamsobjectnullSent with the request.
valueFieldstring'id'Field getValue() returns.
textFieldstring'text'Field shown in the box.
formatterfunction(row)HTML for one list item. The box shows its plain text.
editablebooleantruefalse means pick from the list only.
panelHeightnumber300Maximum dropdown height in pixels.
limitnumber300Items rendered at once; the rest are reachable by typing.
label / labelWidth / prompt / requiredAs for textbox.
onChangefunction(value, row)After a pick.
onSelectfunction(row)The row that was clicked.
onLoadSuccessfunction(rows)After loadData or a fetch.

Methods

MethodReturnsDescription
getValue() / setValue(v)any / thisThe valueField value.
getText()stringWhat the user reads.
loadData(rows)thisSwap the list without touching the value.
reload(url?)PromiseFetch again, optionally from a new URL.
getData()object[]The current list.
clear()this 

Keyboard: arrows move through the list, Enter picks, Escape closes.

datebox

A text field with a month calendar. It reads and writes ISO dates whatever the locale displays.

Options

OptionTypeDefaultDescription
valuestring | DateISO, a Date, or a string in the display format.
formatstringlocaleOverrides the locale's format, from yyyy, yy, mm, dd.
min / maxstring''ISO bounds; days outside them are disabled.
showTodaybooleantrueThe shortcut under the calendar.
label / labelWidth / prompt / requiredAs for textbox.
onChange / onSelectfunction(iso)Both receive the ISO date.

Methods

MethodReturnsDescription
getValue()stringISO yyyy-mm-dd, or ''.
setValue(v)thisAccepts ISO, a Date, or the display format.
getText()stringWhat is on screen, e.g. 09/03/2567.
clear()this 

checkbox

A real <input type="checkbox"> drawn as a switch, so forms submit it and getValue() is simply its checked state.

OptionTypeDefaultDescription
labelstring''Caption beside the switch; also its accessible name.
checkedbooleanInitial state.
disabledboolean 
onChangefunction(checked) 

Methods: getValue(), setValue(v), check(), uncheck(), toggle(), enable(), disable().

filebox

A drop zone and a file list. Drag files onto it or click to browse; it checks them against accept, maxSize and maxFiles, lists what survived, and — if you give it a url — uploads each one with a progress bar.

<div class="kob-filebox" data-options="accept:'.pdf,.png',maxSize:2000000"></div>
var box = Kob.filebox('#docs', { accept: '.pdf,image/*', maxSize: 2 * 1024 * 1024, url: '/api/upload', fieldName: 'document', data: { tenantId: 42 }, onProgress: function (file, percent) { … }, onComplete: function (ok, failed) { … } }); box.upload(); // returns a Promise<{ ok, failed }>
Leave url out and the widget never touches the network: it collects files and getFiles() hands them to your own FormData, which is what you want when the files ride along with the rest of a form.
OptionTypeDefaultWhat it does
multiplebooleantrueAccept more than one file.
acceptstring''Extensions and MIME patterns: '.pdf,image/*'.
maxSizenumber0Bytes per file. 0 means no limit.
maxFilesnumber0How many may be queued at once.
urlstring''POST target. Empty leaves it a picker.
fieldNamestring'file'Form field name each file is sent under.
dataobjectnullExtra form fields sent with every file.
headersobjectnullRequest headers, e.g. an Authorization token.
autoUploadbooleanfalseUpload as soon as a file is added.
text / hintstring''Drop-zone caption and sub-line. Both default to the locale strings.
onAdd / onRemovefunctionnull(file).
onProgressfunctionnull(file, percent) while uploading.
onSuccessfunctionnull(file, response) — parsed as JSON when it is JSON.
onErrorfunctionnull(file, message) — also fires for a file the rules turned away.
onCompletefunctionnull(ok, failed) once every queued file has finished.

Methods: add(files), remove(index), clear(), getFiles(), getItems() (each with its status and percent), upload(), enable() / disable().

Uploads go through XMLHttpRequest rather than fetch, because fetch still cannot report upload progress — and the progress bar is most of the point of a file field. One request per file, so a single failure does not lose the batch.

linkbutton

Turns an <a> or <button> into a toolbar button with an icon.

Colours

Five colours, in two weights. Solid is for the one action a screen is really about — Save, Delete. Soft is the same meaning at a fraction of the weight, for the row of secondary actions beside it, so a toolbar of six buttons does not end up shouting six things at once.

<a class="kob-linkbutton kob-linkbutton-danger">Delete</a> <a class="kob-linkbutton kob-linkbutton-danger kob-linkbutton-soft">Delete</a>
Kob.linkbutton('#save', { text: 'Save', iconCls: 'icon-save', color: 'accent' }); Kob.linkbutton('#del', { text: 'Delete', color: 'danger', soft: true });
colorClassFor
'accent'kob-linkbutton-accentThe main action. primary: true is the older spelling of the same thing.
'success'kob-linkbutton-successApprove, confirm, mark done.
'danger'kob-linkbutton-dangerDelete, and anything else that cannot be undone.
'warn'kob-linkbutton-warnVoid, reverse, override.
'info'kob-linkbutton-infoSend, export, anything informational.

Add soft: true or kob-linkbutton-soft for the tonal version. The same five work on a plain <button class="kob-btn kob-btn-danger"> with no JavaScript at all.

Each colour is a pair of tokens — --kob-danger-solid for the fill and --kob-on-danger for the label — rather than the plain --kob-danger, which is tuned to read as coloured text on the page and goes pale in a dark theme. Soft buttons use a third, --kob-danger-text. Every pair clears 4.5:1 in all three themes.
OptionTypeDefaultDescription
textstringCaption. Omit to keep what is already in the tag.
iconClsstring''e.g. 'icon-save'. See Icons.
iconAlign'left' | 'right''left' 
primarybooleanfalsePaints it in the accent colour.
plainbooleanfalseNo border or background until hovered.
togglebooleanfalseBehaves as a pressable toggle.
selectedbooleanfalseToggle state.
disabledbooleanfalse 
onClickfunction(event) 

Methods: setText(s), getText(), setIcon(cls), toggle(on?), isSelected(), enable(), disable().

form

Collects every kob-ui field inside a <form> so you can read, fill, validate and clear them in one call — whatever mix of widgets they are.

MethodReturnsDescription
getValues()objectKeyed by each field's name, read through its widget.
setValues(obj)thisFills matching fields; unknown keys are ignored.
validate()booleanMarks offenders with .kob-invalid and focuses the first.
clear()thisEmpties every field and drops the marks.
OptionTypeDescription
markInvalidbooleanAdd .kob-invalid to failing fields. Default true.
onValidatefunction(ok, values)Return false to fail validation with your own rule.
onSubmitfunction(event, values)Runs after validation passes on a real submit.

panel

A titled content box that can fold away or load its body from a URL — and parse the widgets in whatever comes back.

OptionTypeDefaultDescription
titlestring''Header caption. Omit for a headerless panel.
iconClsstring''Icon in the header.
collapsiblebooleanfalseShows the fold caret.
collapsedbooleanfalseStart folded.
closablebooleanfalseShows a close button that removes the panel.
hrefstring''Fetch this URL into the body on init.
fitbooleanfalseFill the parent box.
borderbooleantrue 
loadingMessagestringlocaleShown while href is in flight.
onLoad / onCollapse / onExpand / onClosefunction 

Methods: getBody(), setTitle(s), load(url), setContent(html), collapse(), expand(), toggleCollapse(), close().

layout

The classic five-region frame. Regions are declared on the children; the widget restacks them once so flexbox can do the sizing.

Region options

Set these in each child's data-options.

OptionTypeDescription
regionstringnorth, south, west, east or center. Required.
widthnumberwest and east, in pixels.
heightnumbernorth and south, in pixels.
splitbooleanAdds a draggable splitter on the inner edge.
minSize / maxSizenumberDrag limits.
collapsiblebooleanPuts a fold button on the splitter.

Methods: panel(region) returns the region element, plus resize(region, px), collapse(region), expand(region), toggle(region), remove(region).

Events: onResize(region, size), onCollapse(region), onExpand(region).

A layout fills its parent, so give the parent a height — height:100% or a fixed value — or it collapses to nothing.

tabs

Every direct child carrying a title attribute becomes a page.

OptionTypeDefaultDescription
selectednumber0Index shown first.
closablebooleanfalseClose buttons on every tab. A page can opt in on its own with data-options="closable:true".
onSelectfunction(index, title) 
onBeforeClosefunction(index, title)Return false to keep the tab open.
onClose / onAddfunction 

Methods: select(i), selectByTitle(s), getSelected(), getPanel(i), getPages(), add({ title, content, closable }), close(i).

dialog and modal

Built around an element that is already on the page: it keeps its content and becomes the window body, with the frame added around it.

OptionTypeDefaultDescription
titlestring'' 
widthnumber460 
heightnumber | 'auto''auto' 
modalbooleantruefalse makes it a draggable, resizable window.
closedbooleantrueStart hidden; call open().
buttonsarraynull{ text, iconCls, primary, handler(dialog) }.
draggable / resizablebooleantrueNon-modal windows only.
minimizable / maximizablebooleantrueNon-modal windows only.
sheetbooleanfalseDocks to the bottom edge — the shape phones expect.
minWidth / minHeightnumber300 / 120Resize limits.
onOpen / onClose / onResize / onMove / onMinimize / onMaximizefunctiononMinimize returning false keeps the window open.

Methods: open(), close(), isOpen(), setTitle(s), setButtons(list), maximize(on?), restore(), minimize(), center(), resize({width,height}), move({left,top}), window(), getBody(), destroy().

Two spellings

The modal flag decides everything, so it has a name at each end and you never have to remember which way round it goes:

Kob.modal(el, { title: 'Edit' }); // dialog({ modal: true }) Kob.window(el, { title: 'Invoices' }); // dialog({ modal: false })

Dialog markup is usually hidden with style="display:none" so it cannot flash before the widget is built. The widget clears that when it takes the element over — the frame owns visibility from then on.

card

CSS only — there is no behaviour to attach, so there is no Kob.card(). Compose the parts you need.

<div class="kob-card kob-card-hover"> <div class="kob-card-header"> <span class="kob-icon icon-home"></span>Sukhumvit <div class="kob-card-tools"><span class="kob-badge kob-badge-success">92%</span></div> </div> <img class="kob-card-media" src="…" alt=""> <div class="kob-card-body">46 of 50 rooms occupied.</div> <div class="kob-card-footer"> <span class="kob-muted kob-small">Updated today</span> <span class="kob-spacer"></span> <a class="kob-linkbutton icon-forward"><span class="kob-icon"></span>Open</a> </div> </div>
ClassDescription
kob-cardThe box. A flex column, so the body stretches.
kob-card-headerTitle row. kob-card-tools inside pushes to the right.
kob-card-mediaFull-bleed image at the top.
kob-card-bodyContent. First and last child margins are collapsed away.
kob-card-footerAction row on the accent tint.
kob-card-hoverLifts on hover — for cards that are links.

ribbon

Tabs of grouped commands. One large button, or a stack of small ones; a button with a menu opens it instead of firing.

OptionTypeDefaultDescription
tabsarraynull[{ title, groups: [{ title, items: [...] }] }]
selectednumber0Tab shown first.
collapsiblebooleantrueClicking the open tab folds the ribbon away.
collapsedbooleanfalseStart folded.
onSelect / onClickfunction(index, tab) / (item)

Item keys: text, iconCls, size: 'large' | 'small', disabled, menu, onClick. Consecutive small buttons stack three to a column.

Methods: select(i), collapse(), expand(), toggleCollapse(), setTabs(tabs).

tree

Nodes are plain objects. Children may be an array, or left out with state: 'closed' to be fetched the first time the node opens.

Node shape

{ id: 1, text: 'Sukhumvit', iconCls: 'icon-home', state: 'open', // or 'closed' checked: false, children: [ /* … */ ] // omit + state:'closed' to load lazily from `url` }
OptionTypeDefaultDescription
dataTreeNode[]nullThe root nodes.
urlstring''Root through Kob.config.request; lazy children are fetched from the same URL with id.
idField / textField / childrenFieldstringid / text / childrenRead your own shape without remapping.
checkboxbooleanfalseA tick box on every node.
cascadeCheckbooleantrueFolders tick their children; partly-ticked folders show a dash.
linesbooleanfalseGuide lines down each branch.
expandDepthnumber1How deep to start open.
formatterfunction(node)HTML for one label.
onSelect / onClick / onDblClickfunction(node) 
onExpand / onCollapsefunction(node) 
onCheckfunction(node, checked) 
onContextMenufunction(event, node)Pairs with menu.showAt().

Methods: loadData, reload, expand(node), collapse(node), toggle(node), expandAll(), collapseAll(), select(node), getSelected(), find(id), check(node, on), getChecked(), walk(nodes, fn), getData().

desktop

A windowed shell: wallpaper with launcher icons, a taskbar, a start menu, and one window per running app. Each window is a non-modal dialog, so dragging, resizing and maximising come from that widget — what the desktop adds is knowing what is running, which window has focus, and how to get a minimised one back.

Apps

An app says what its window contains, in one of three ways.

KeyTypeDescription
idstringRequired. What open() and close() take.
titlestringWindow title, icon caption and taskbar label.
iconClsstringIcon from the built-in set.
iconstringAn image URL instead, for a real app icon.
contentstringHTML for the window body. Widgets in it are parsed.
hrefstringFetch a fragment into the body instead.
onOpenfunction(win, body)Or build the contents yourself.
width / heightnumberWindow size.
singlebooleanDefault true — opening again focuses the existing window.
hiddenbooleanReachable through open(), but not shown as an icon.

Options

OptionTypeDefaultDescription
appsApp[]nullAs above.
iconsbooleantrueLauncher icons on the wallpaper.
taskbarbooleantrueThe bar along the bottom.
startLabelstring | false'Start'Start button text; false hides it.
startExtrasMenuItem[]Entries under the app list.
clockbooleantrueClock at the right.
userobject{ name, iconCls, onLogout }
wallpaperstringAny CSS background.
onOpen / onClose / onFocusfunction(app, win) 

Methods: open(id) returns the window's Dialog, plus close(id), closeAll(), minimize(id), getRunning(), getWindow(id), setApps(apps), setWallpaper(css).

The desktop fills its parent, so give that parent a height. On a full page that means html, body { height: 100% }.

datagrid

The whole result set lives in memory, and searching, filtering, sorting and paging all happen locally — so changing a filter never costs another round trip.

Filter chips

Above the header sits one chip per column. Each decides what to offer by looking at the data: a pick-list for categories, a lower bound for numbers, a contains box for free text.

They filter what the reader 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.

Inline editing

Options

OptionTypeDefaultDescription
columnsarraynullSee below. A nested [[…]] array is accepted for EasyUI-shaped configs.
dataarray | objectnullRows, { rows } or { data }.
urlstring''Fetched through Kob.config.request.
queryParamsobjectnullSent with the request.
loadFilterfunction(payload)Reshape the payload before it is read.
paginationbooleantrueThe footer with page controls.
pageSizenumber50 
pageListnumber[][20,50,100]Choices in the rows-per-page menu.
searchablebooleantrueThe free-text box in the footer.
filterablebooleantrueThe chip row above the header.
rownumbersbooleanfalse 
showHeaderbooleantrue 
stripedbooleantrue 
singleSelectbooleantruefalse lets clicks accumulate a selection.
fitbooleanfalseFill the parent's height.
emptyMsgstringlocaleShown when nothing matches.

Column definition

KeyTypeDescription
fieldstringProperty on the row.
titlestringHeader text.
widthnumberPixels. Columns that fit are stretched to fill the grid.
align'left' | 'center' | 'right' 
sortablebooleanfalse makes the header inert.
formatterfunction(value, row, index)Returns the cell's HTML.
editorstring | object'text', 'numberbox', or { type, options: { precision } }.
filterfalse | stringfalse drops the chip; a string forces its kind: text, number, enum, bool.

Methods

MethodReturnsDescription
loadData(rows)thisReplace the rows.
reload(url?)PromiseFetch again. Accepts { url, queryParams }.
getRows()Row[]Everything loaded.
getVisibleRows()Row[]After search, filters and sort.
getSelected()Row | null 
getSelections()Row[]For singleSelect: false.
selectRow(i) / clearSelections()this 
sort(field, order?)thisOmit the order to flip it.
setFilter(field, value)this{ in: [...] }, { ge: n }, { q: 'text' }, { eq: v }, or null to drop it.
getFilters() / clearFilters()object / this 
search(text)thisAs if typed into the footer box.
gotoPage(n) / pageCount()this / number 
beginEdit(i) / endEdit(i) / cancelEdit()this 
loading() / loaded()thisShow or hide the mask by hand.
resize()thisRe-measure columns. Rarely needed — it watches its own box.

Events

EventArguments
onClickRow / onDblClickRow / onSelect(index, row)
onLoadSuccess / onLoadError(data) / (error)
onSortChange(field, order)
onFilterChange(filters)
onEndEdit(index, row)

Editing and deleting from the row

Not every table has room for a column of buttons. rowActions gives you the actions without one: a strip that appears over a row when the pointer is on it, 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: function (row) { return row.status === 'paid'; }, handler: remove } ], // The other half of the same idea — most people try this first. onDblClickRow: function (i, row) { edit(row); } });
KeyTypeWhat it does
textstringButton caption. Leave it out for an icon-only button; the label then comes from title.
iconClsstringe.g. 'icon-remove'.
colorstringOne of the five button colours.
softbooleanDefaults to true — a strip of solid buttons over a row is too loud.
disabledboolean | function(row, index). Re-evaluated for every row the strip lands on.
handlerfunction(row, index), with the grid element as this.

rowActionsOn: 'selected' drops the hover behaviour and shows the strip only on the selected row. Methods showRowActions(tr, hovering) and hideRowActions() drive it by hand.

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 — one or the other has to give.

messager and toasts

Each helper returns a promise, and still calls an 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' }); } const name = await Kob.prompt({ title: 'Rename', msg: 'New name:', value: current }); if (name !== null) { rename(name); }
CallResolves to
Kob.alert({ title, msg, html, icon })true
Kob.confirm({ title, msg })true or false
Kob.prompt({ title, msg, value })the typed string, or null
Kob.toast({ msg, type, timeout }){ close(), element }

icon takes 'info', 'warn' or 'error'; toast type takes 'success', 'error', 'warn' or 'info'. Pass html: true to let msg carry markup.

sweet

The big centred confirmation. Kob.alert and Kob.confirm draw a titled window, which is right for the dozens of small confirmations a data-entry app asks for. This is the other shape: one decision, front and centre, with a coloured mark that says at a glance whether the news is good.

await Kob.sweet.success('Saved', 'Invoice 2026-0042 has been issued.'); if (await Kob.sweet({ type: 'question', title: 'Delete this tenant?', msg: 'Room 112/3 becomes vacant. This cannot be undone.', showCancel: true, confirmText: 'Delete', cancelText: 'Keep', danger: true })) { await api.remove(id); }
It resolves true for confirm and false for everything else — cancel, Escape, a click on the backdrop, the timer running out — so if (await Kob.sweet(…)) is always safe to write. It never resolves undefined.
OptionTypeDefaultWhat it does
typestring'info'success, error, warn, info, question, or '' for no mark.
title / msgstring''Heading and body.
htmlbooleanfalseTreat msg as markup. Only pass markup you produced yourself.
showCancelbooleanfalseAdd the second button.
confirmText / cancelTextstringlocaleName the buttons after what they do, not OK and Cancel.
dangerbooleanfalsePaint the confirm button as destructive.
timernumber0Auto-dismiss after n ms, resolving false.
closeOnBackdropbooleantrueA click on the dimmed page dismisses.

Kob.sweet.success, .error, .warn, .info and .question are shorthands taking (title, msg) or the same options object.

statusbar

The strip along the bottom of a screen: what the app is doing, how many rows there are, who is signed in. CSS only — there is no widget to build and nothing to destroy.

<div class="kob-statusbar"> <span class="kob-status-item kob-status-ok"><i class="kob-status-dot"></i>Ready</span> <span class="kob-status-sep"></span> <span class="kob-status-item"><span class="kob-icon icon-table"></span><b>84</b> rows</span> <span class="kob-spacer"></span> <span class="kob-status-item"><span class="kob-icon icon-user"></span>admin</span> </div>
ClassWhat it is
kob-statusbarThe strip. Put it last inside a bordered box, or in a layout's south region.
kob-status-itemOne entry. Aligns an icon, a dot and text on one line.
kob-status-dotA coloured dot inside an item.
kob-status-sepA vertical rule between groups.
kob-spacerPushes everything after it to the right.
kob-status-ok / -busy / -badOn an item: colours its text and its dot green, amber or red.
The state classes colour the dot and the label, and the label still says what the state is. Colour on its own would leave the state invisible to a reader who cannot separate green from amber.

React, Vue and Angular

kob-ui is not a component library — it builds DOM imperatively. That works inside any framework, on one condition: give it an element the framework will leave alone, and destroy the widget when that element goes away.

The rule. Render an empty container, hand it to kob-ui, and never render children into it yourself. Everything inside belongs to the widget; everything outside belongs to your framework.

destroy() takes back every node the widget added and returns the element as it found it, so mounting again on the same element is safe — which is what React's StrictMode does in development, and what any router does when you revisit a screen.

React

import { useEffect, useRef } from 'react'; import Kob from 'kob-ui'; import 'kob-ui/css'; /** Builds a widget on mount and destroys it on unmount. */ export function useKob(widget, options, deps = []) { const ref = useRef(null); const instance = useRef(null); useEffect(() => { instance.current = Kob[widget](ref.current, options); return () => { instance.current.destroy(); instance.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); return [ref, instance]; }
function OrderGrid({ onPick }) { const [ref, grid] = useKob('datagrid', { columns: [ { field: 'room', title: 'Room', width: 100 }, { field: 'tenant', title: 'Tenant', width: 180 } ], url: '/api/rooms', onClickRow: (i, row) => onPick(row) }); // Drive it later through the ref — never by re-rendering into the div. const refresh = () => grid.current.reload(); return ( <>
); }

Vue 3

<script setup> import { ref, onMounted, onBeforeUnmount, shallowRef } from 'vue'; import Kob from 'kob-ui'; import 'kob-ui/css'; const host = ref(null); const grid = shallowRef(null); // shallowRef: never make a widget reactive onMounted(() => { grid.value = Kob.datagrid(host.value, { columns: [{ field: 'room', title: 'Room', width: 100 }], url: '/api/rooms' }); }); onBeforeUnmount(() => grid.value?.destroy()); defineExpose({ reload: () => grid.value.reload() }); </script> <template> <div ref="host" style="height: 420px"></div> </template>

Angular

import { Directive, ElementRef, Input, OnInit, OnDestroy } from '@angular/core'; import Kob from 'kob-ui'; @Directive({ selector: '[kobDatagrid]', standalone: true }) export class KobDatagridDirective implements OnInit, OnDestroy { @Input('kobDatagrid') options: any = {}; private grid: any; constructor(private host: ElementRef<HTMLElement>) {} ngOnInit() { this.grid = Kob.datagrid(this.host.nativeElement, this.options); } ngOnDestroy() { this.grid?.destroy(); } reload() { this.grid.reload(); } } // <div [kobDatagrid]="{ columns, url: '/api/rooms' }" style="height:420px"></div>

Widget callbacks fire outside Angular's zone by default. If a change detection cycle needs to follow one, wrap it: this.zone.run(() => …).

Things to watch

SituationWhat to do
Data changed Call grid.loadData(rows) from an effect or watcher. Do not rebuild the widget.
Options changed Kob.datagrid(el, { pageSize: 100 }) merges and refreshes in place.
A cell needs a framework component A formatter returns an HTML string, not JSX. For rich cells, render into the row element from onLoadSuccess, or keep the cell simple and open a dialog on click.
Server rendering Widgets touch the DOM, so build them in a client-only lifecycle hook — useEffect, onMounted, ngOnInit in the browser.
Strict mode double mount Handled: destroy() restores the element, so the second mount produces exactly one widget.

filterbutton

The datagrid's filter chip, on its own — so a filter bar can sit above a chart, a card list, or anything else that is not a grid.

OptionTypeDefaultDescription
labelstring''Shown before the value: "Building: All".
kind'enum' | 'text' | 'number''enum'Tick list, contains box, or lower bound.
optionsarraynullenum only: ['Paid', 'Pending'] or [{ value, text }].
valuestring[] | string | numbernullStarting value.
multiplebooleantruefalse makes the list single-choice.
onChangefunction(value)string[] for enum, number, string, or null.

Methods: getValue(), setValue(v), clear(), setOptions(list).

alert

The banner that stays on the page — a validation summary above a form, a warning across the top of a screen.

Two different things share the word. Kob.alert() opens a modal dialog. This inline banner is Kob.alertbox(), or class="kob-alert" in markup.

OptionTypeDefaultDescription
type'info' | 'success' | 'warn' | 'danger''info'Colour and icon.
titlestring''Bold line above the message.
msgstringBody. Omit to keep what is already in the markup.
htmlbooleanfalseTreat title and message as HTML.
iconClsstring | falseby typeOverride or remove the icon.
closablebooleanfalseAdds a dismiss button.
timeoutnumber0Dismiss by itself after this many ms.
onClosefunction 

Methods: setMessage(s), setType(t), close().

badge

Static badges are markup. The widget is for the ones that change — an unread count on a toolbar button, a status that flips as data arrives.

<span class="kob-badge kob-badge-success">Paid</span> <span class="kob-badge kob-badge-danger">Overdue</span> <span class="kob-badge kob-badge-dot kob-badge-warn"></span>
const unread = Kob.badge('#inbox-count', { type: 'danger', max: 99 }); unread.increment(); // 3 -> 4 unread.setText(250); // shows 99+ unread.setText(0); // hides itself
OptionTypeDefaultDescription
textstring | numbermarkupWhat is shown.
typestring'muted'muted, accent, success, warn, danger, info.
dotbooleanfalseA coloured dot with no text.
maxnumber99Counts above this show as "99+".
hideEmptybooleantrueHide at 0 or empty.

Methods: setText(v), getText(), setType(t), increment(by).

progressbar

A measured fill, or a sweeping stripe while the length of the job is unknown.

OptionTypeDefaultDescription
valuenumber00-100, clamped.
indeterminatebooleanfalseSweeping stripe; no percentage shown.
textstring | false'{value}%'Label template; false for none.
heightnumber20Bar height in pixels.
typestring'accent'accent, success, warn, danger.
stripedbooleanfalseMoving stripes on the fill.
onChange / onCompletefunctiononComplete fires once, when the value first reaches 100.

Methods: setValue(n), getValue(), indeterminate(on).

preloader and spinner

Over an element it is a busy mask; over <body> it is the splash the page shows before the application has drawn anything.

// The one-liner: cover, work, uncover. const done = Kob.loading('#panel', 'Fetching invoices...'); await api.load(); done(); // Or keep the widget and drive it. const splash = Kob.preloader(document.body, { size: 'lg', msg: 'Starting' }); splash.setMessage('Loading settings'); splash.hide();

The spinner on its own is a class, with three sizes:

<span class="kob-spinner kob-spinner-sm"></span> <span class="kob-spinner kob-spinner-md"></span> <span class="kob-spinner kob-spinner-lg"></span>
OptionTypeDefaultDescription
msgstring''Caption under the spinner.
size'sm' | 'md' | 'lg''md' 
visiblebooleantruefalse builds it hidden.
backdropbooleantrueDim what is underneath.
timeoutnumber0Hide by itself after this many ms.

Methods: show(msg), hide(), isVisible(), setMessage(s).

A static element gets position: relative so the cover has something to sit against; destroy() puts that back.

Grid, flex and button groups

Enough layout CSS to lay out a form or a dashboard without reaching for a whole framework. Deliberately small.

The 12-column row

<div class="kob-row"> <div class="kob-col-8">main</div> <div class="kob-col-4">side</div> </div>

Below 720px every column takes the full width — one honest breakpoint rather than a matrix of per-size classes. For cards and tiles, kob-autogrid fits as many columns as it can:

<div class="kob-autogrid">…</div> <!-- min 260px --> <div class="kob-autogrid kob-autogrid-sm">…</div> <!-- min 180px --> <div class="kob-autogrid" style="--kob-min:320px">…</div>

Flex helpers

ClassDoes
kob-flex / kob-inline-flexFlex container with the standard gap.
kob-flex-col / kob-flex-wrapDirection and wrapping.
kob-items-start / -center / -end / -baseline / -stretchCross-axis alignment.
kob-justify-start / -center / -end / -between / -aroundMain-axis alignment.
kob-grow / kob-noneTake the slack, or refuse to.
kob-spacerAn empty element that pushes what follows to the far end.
kob-gap-0 … kob-gap-5Gap scale: 0, 6, 10, 14, 20, 28px.
kob-p-0 … kob-p-4, kob-mt-*, kob-mb-*Padding and vertical margin on the same scale.
kob-fill / kob-w-full / kob-h-full / kob-scrollSizing and overflow.
kob-truncate / kob-muted / kob-small / kob-strong / kob-text-*Text.

Button groups and toolbars

<div class="kob-btn-group"> <button class="kob-btn selected">Rows</button> <button class="kob-btn">Cards</button> <button class="kob-btn">Chart</button> </div> <div class="kob-toolbar"> <a class="kob-linkbutton icon-add"><span class="kob-icon"></span>New</a> <span class="kob-sep"></span> <a class="kob-linkbutton icon-print"><span class="kob-icon"></span>Print</a> <span class="kob-spacer"></span> <a class="kob-linkbutton icon-settings"><span class="kob-icon"></span></a> </div>

Keep selected on one member and the group reads as a segmented control. kob-btn-group-vertical stacks it.

Custom widgets

Extend Kob.Widget and register it. From then on it behaves like a built-in: Kob.parse() builds it from markup, data-options works, and the accessor returns the instance.

MemberDescription
static widgetNameDrives the class name: 'rating'class="kob-rating".
static defaultsMerged under data-options and the constructor argument.
this.elThe element the widget is mounted on.
this.optionsThe resolved options.
init()Build the DOM. Called by the constructor.
refresh()Called when new options arrive after construction.
track(off)Register a cleanup function — pass the unbinder Kob.dom.on returns.
emit(hook, ...args)Fire an option callback with the element as this.
destroy()Runs every tracked cleanup. Extend it, then call super.destroy().

Kob reference

MemberDescription
Kob.versionThe library version.
Kob.parse(root) / unparse(root) / autoParse()Build or tear down the widgets declared in markup.
Kob.parseOptions(str)Read the inline data-options syntax.
Kob.instance(el, name)The widget of that name on an element, if any.
Kob.configrequest, onRequestError, prefix, zIndex, locale.
Kob.locale(name?) / addLocale(name, bundle) / locales / t(key, vars)Localisation.
Kob.zIndex()Next z-index for a floating layer, shared by every widget.
Kob.Widget / register(Class) / registryExtension points.
Kob.domThe internal DOM helpers: on, el, append, css, escapeHtml, …
Kob.dateparseDate, formatDate, toISO, today.
Kob.classesThe widget classes themselves, for extends.