CtrlK Documentation
The first IOUX (Integrated Operational UX) for enterprise web apps. 19 modules, 150 tests, zero dependencies.
CtrlK is an interaction layer that sits between your users and your application. It adds command palette, keyboard shortcuts, saved views, persistent selections, field navigation, macros, undo, and shareable view links — on top of any existing enterprise web app. No redesign required.
Installation
# npm npm install @ctrlk/core # yarn yarn add @ctrlk/core # CDN (Pattern A drop-in) <script src="https://unpkg.com/ctrlk/dist/ctrlk.runtime.min.js"></script>
Framework-specific packages:
npm install @ctrlk/react # React hooks + Provider npm install @ctrlk/angular # Service + directives npm install @ctrlk/ag-grid # AG Grid adapter
Quick Start
30-Second Setup (Pattern A)
<!-- Add before </body>. That's it. --> <script src="ctrlk.runtime.min.js"></script> <!-- Result: · Ctrl+K opens command palette · All buttons auto-discovered as commands · Ctrl+D cycles density · Every section and link is searchable -->
Programmatic Setup (Pattern B)
import ctrlk from 'ctrlk'; // Initialize with options ctrlk.init({ palette: true, // Enable command palette UI density: true, // Enable density controller autoDiscover: true, // Auto-register DOM elements session: true, // Enable session tracking macros: true, // Enable macro recording history: true, // Enable undo/redo }); // Register a command ctrlk.commands.register({ id: 'filter.active', title: 'Show Active Only', shortcut: 'Alt+A', category: 'Filters', icon: '✅', execute: () => applyFilter('active'), });
Integration Patterns
| Pattern | Setup Time | Integration Depth | Best For |
|---|---|---|---|
| A — Drop-In | 30 seconds | Auto-discovery only | Quick evaluation, legacy apps |
| B — Declarative | 1-2 hours | Custom commands + views | Most applications |
| C — Framework | Half day | Hooks, directives, adapters | React/Angular/Vue apps with grid libraries |
Each pattern extends the previous. Start with A, evolve to C over time. No migration or rewrite needed.
EventBus Core
The internal event system. All modules communicate through this bus. You rarely call it directly, but you can subscribe to any event for custom behavior.
| Method | Parameters | Returns | Description |
|---|---|---|---|
on(event, handler) | string, Function | Function (unsubscribe) | Subscribe to an event. Returns cleanup function. |
emit(event, data) | string, any | void | Emit an event with optional data payload. |
off(event?) | string? | void | Remove all listeners for an event, or all events if no argument. |
// Listen for any command execution const unsub = ctrlk.bus.on('command:executed', ({ id }) => { console.log(`Command executed: ${id}`); }); // Later: clean up unsub();
CommandRegistry Core
The central registry of all commands. Every action in CtrlK is a command — palette entries, shortcuts, macro steps, and history entries all reference commands by ID.
| Method | Parameters | Returns | Description |
|---|---|---|---|
register(definition) | CommandDef | Function (unregister) | Register a command. Returns cleanup function. |
execute(id, ...args) | string, ...any | any | Execute a command by ID. |
search(query) | string | CommandDef[] | Fuzzy search commands by title or ID. |
get(id) | string | CommandDef? | Get a command definition by ID. |
has(id) | string | boolean | Check if a command exists. |
list(category?) | string? | CommandDef[] | List all commands, optionally filtered by category. |
unregister(id) | string | boolean | Remove a command by ID. |
CommandDef Object
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique command ID (e.g. 'view.save') |
title | string | Yes | Display title in palette |
execute | Function | Yes | Function to run when command is invoked |
shortcut | string | No | Keyboard shortcut (e.g. 'Ctrl+Shift+S') |
category | string | No | Group label in palette (e.g. 'Views') |
icon | string | No | Emoji or icon for palette display |
undo | Function | No | Undo function (enables Ctrl+Z for this command) |
when | Function | No | Condition function — command only available when this returns true |
Events emitted: command:registered command:executed command:unregistered
ShortcutEngine Core
Scope-aware keyboard shortcut binding. Supports modifier keys, key chords (sequences), and scope-based activation (e.g. a shortcut only active when a grid is focused).
| Method | Parameters | Returns | Description |
|---|---|---|---|
bind(shortcut, commandId) | string, string | Function (unbind) | Bind a shortcut to a command. |
unbind(shortcut) | string | void | Remove a shortcut binding. |
setScope(scope) | string | void | Set the active scope (e.g. 'grid', 'detail'). |
getScope() | — | string | Get the current scope. |
Shortcut Format
'Ctrl+K' // Single shortcut 'Ctrl+Shift+S' // Multiple modifiers 'Alt+1' // Alt + number 'F2' // Function key 'Ctrl+K Ctrl+S' // Chord (press Ctrl+K, then Ctrl+S) 'Escape' // Named key
Events emitted: shortcut:fired shortcut:chord-started
CommandPalette Core
The Ctrl+K searchable command UI. Renders a floating overlay with fuzzy search, keyboard navigation, category grouping, and shortcut display.
| Method | Description |
|---|---|
inject() | Create and inject the palette DOM elements into the page. |
show() | Open the palette. |
hide() | Close the palette. |
toggle() | Toggle open/close. |
isOpen() | Returns true if palette is currently visible. |
Events emitted: palette:opened palette:closed palette:selected
DensityController Core
Controls information density across the application. Cycles between compact, comfortable, and spacious by setting CSS custom properties on the document root.
| Method | Description |
|---|---|
init() | Initialize with default level (reads from localStorage if saved). |
cycle() | Advance to the next density level. |
set(level) | Set a specific level: 'compact', 'comfortable', or 'spacious'. |
get() | Returns the current density level string. |
CSS Custom Properties Set
| Property | Compact | Comfortable | Spacious |
|---|---|---|---|
--ctrlk-density | compact | comfortable | spacious |
--ctrlk-row-height | 28px | 36px | 48px |
--ctrlk-cell-padding | 2px 8px | 6px 12px | 10px 16px |
--ctrlk-font-size | 12px | 14px | 16px |
Events emitted: density:changed
AutoDiscovery Core
Scans the DOM and auto-registers elements as commands. Buttons get their text content as the title, links get their href, sections get navigation commands. This is what makes Pattern A work with zero configuration.
| Method | Description |
|---|---|
start() | Begin scanning and watching for DOM changes (uses MutationObserver). |
stop() | Stop watching for DOM changes. |
rescan() | Manually re-scan the DOM for new elements. |
<button>, <a>, and [data-ctrlk] elements → registers them as searchable commands in the palette.ViewStateManager State
Save, restore, and share complete application view states. A "view" captures grid columns, filters, sort, scroll position, density, and any custom app state. Default limit: 5 saved views with LRU eviction.
| Method | Parameters | Returns | Description |
|---|---|---|---|
save(name, options?) | string, {description?, scope?, overwrite?} | ViewState | Save current state as a named view. Evicts LRU if at max. |
load(name) | string | boolean | Restore a saved view. Updates lastUsed timestamp. |
has(name) | string | boolean | Check if a named view exists. |
get(name) | string | ViewState? | Get a saved view without applying it. |
delete(name) | string | boolean | Delete a saved view. |
list() | — | ViewState[] | List all saved views. |
getSlots() | — | Array | All views with slot numbers and shortcuts (Ctrl+1 through Ctrl+N). |
capture() | — | Object | Capture current state (grid + app providers) without saving. |
autoSave() | — | void | Auto-save current state (restored on next init). |
autoRestore() | — | boolean | Restore the auto-saved state. |
setMaxViews(n) | number | void | Change max saved views. Evicts if currently over limit. |
getMaxViews() | — | number | Get current max (default: 5). |
setGridAdapter(adapter) | GridAdapter | void | Connect a grid adapter for state capture/restore. |
registerProvider(key, provider) | string, {capture, restore} | Function | Register custom app state. Returns unregister function. |
Constructor Options
new ViewStateManager(bus, { maxViews: 5, // Default: 5. LRU eviction when exceeded. });
view:saved Event Payload
{
name: 'Monday Review',
slot: 2, // Position (1-based)
shortcut: 'Ctrl+2', // Assigned shortcut (null if slot > 9)
totalSaved: 3, // Current count
maxViews: 5, // Configured max
remaining: 2, // Slots available
evicted: 'Old View', // Name of evicted view (null if none)
}Events: view:saved view:loaded view:deleted view:evicted
SelectionModel State
Cross-page persistent selections with named sets and set operations. Selections survive pagination, filtering, and navigation.
| Method | Description |
|---|---|
select(ids) | Add IDs to the current selection. |
deselect(ids) | Remove IDs from the current selection. |
toggle(id) | Toggle a single ID. |
clear() | Clear the entire selection. |
getSelected() | Returns Set of all selected IDs. |
count() | Number of selected items. |
isSelected(id) | Check if a specific ID is selected. |
where(predicate) | Select by expression: .where(row => row.status === 'active') |
saveAs(name) | Save the current selection as a named set. |
loadSet(name) | Restore a named selection set. |
union(setName) | Current selection ∪ named set. |
intersect(setName) | Current selection ∩ named set. |
subtract(setName) | Current selection − named set. |
Events: selection:changed selection:cleared selection:set-saved
FieldRegistry State
Field-level navigation and tracking for detail pages. Registers form fields for jump-to, dirty tracking, empty navigation, and pinning.
| Method | Description |
|---|---|
register(fieldId, options) | Register a field with label, section, element ref. |
jumpTo(fieldId) | Scroll to and focus a field. |
nextEmpty() | Jump to the next empty/unfilled field. |
getDirty() | Returns list of fields that differ from original values. |
pin(fieldId) | Pin a field for cross-record comparison. |
unpin(fieldId) | Unpin a field. |
getPinned() | Returns all pinned field IDs. |
search(query) | Search fields by label text. |
getCompleteness() | Returns { total, filled, empty, percent }. |
Events: field:focused field:dirty field:pinned
GridAdapter Adapter
Abstract interface that grid-specific adapters implement. This is the contract between CtrlK and any grid library.
| Method | Description |
|---|---|
captureState() | Returns the full grid state: columns, filters, sort, scroll. |
restoreState(state) | Apply a previously captured state. |
getColumns() | Returns array of column definitions with visibility. |
setColumnVisible(colId, visible) | Show/hide a column. |
ensureColumnVisible(colId) | Scroll to make a column visible. |
getSelectedRows() | Returns array of selected row IDs. |
flashCells(options) | Flash-highlight cells (for column jump feedback). |
@ctrlk/ag-grid and @ctrlk/devextreme implement this interface. The same CtrlK commands work with either grid — the adapter translates.MacroEngine Power
Record, parameterize, and replay command sequences. A macro is a named list of commands that can be replayed with a single keystroke.
| Method | Description |
|---|---|
startRecording(name) | Begin recording. All subsequent command executions are captured. |
stopRecording() | Stop recording and save the macro. |
play(name, params?) | Replay a saved macro. Optional parameter substitution. |
list() | List all saved macros. |
delete(name) | Delete a saved macro. |
isRecording() | Returns true if currently recording. |
Events: macro:recording-started macro:recording-stopped macro:played
HistoryManager Power
Application-level undo/redo with branching. Tracks commands that define an undo function and allows reversal.
| Method | Description |
|---|---|
undo() | Undo the last undoable command. |
redo() | Redo the last undone command. |
canUndo() | Returns true if undo is available. |
canRedo() | Returns true if redo is available. |
getHistory() | Returns the full history stack. |
clear() | Clear all history. |
Events: history:undo history:redo history:pushed
ActiveFilterBar Power
Renders dismissible filter chips showing all active filters. Each chip has a × button to remove that filter.
| Method | Description |
|---|---|
addFilter(key, label, removeFn) | Add a filter chip. removeFn is called when × is clicked. |
removeFilter(key) | Programmatically remove a filter chip. |
clear() | Remove all filter chips. |
getActive() | Returns array of active filter keys. |
ColumnNavigator Nav
Column search, bookmarks, and horizontal navigation. The module that solves Problem #1 — finding columns in a 150-column grid.
| Method | Description |
|---|---|
search(query) | Search columns by name. Returns matching column definitions. |
jumpTo(colId) | Scroll horizontally to a column and flash-highlight it. |
bookmark(colId) | Toggle bookmark on a column. |
nextBookmark() | Jump to the next bookmarked column. |
prevBookmark() | Jump to the previous bookmarked column. |
getBookmarks() | Returns all bookmarked column IDs. |
setVisibilityProfile(name, colIds) | Define a named column profile. |
applyProfile(name) | Apply a column visibility profile. |
Default shortcuts: Ctrl+G (search), Ctrl+→ / Ctrl+← (bookmark nav)
FocusNavigator Nav
Spatial zone navigation. Defines zones (toolbar, grid, sidebar, detail panel) and cycles between them with F6.
| Method | Description |
|---|---|
addZone(id, element, options?) | Register a focus zone. |
nextZone() | Move focus to the next zone (F6). |
prevZone() | Move focus to the previous zone (Shift+F6). |
focusZone(id) | Focus a specific zone by ID. |
discover() | Auto-discover zones from [data-ctrlk-zone] attributes. |
SessionTracker Nav
Track which records have been visited, reviewed, or marked dirty in a batch workflow. Shows progress (e.g. "12/30 reviewed").
| Method | Description |
|---|---|
markVisited(id) | Mark a record as visited. |
markReviewed(id) | Mark as reviewed (subset of visited). |
markDirty(id) | Mark as having unsaved changes. |
nextUnreviewed() | Returns the ID of the next unreviewed record. |
getProgress() | Returns { visited, reviewed, dirty, total, percent }. |
isReviewed(id) | Check if a specific record is reviewed. |
reset() | Clear all tracking data. |
Events: session:visited session:reviewed session:progress
ViewShare New
Shareable application views. Three tiers: URL links (no server), stored team views (app provides storage), and live sharing (Enterprise).
Tier 1 — URL Links (no server)
| Method | Description |
|---|---|
createLink(options?) | Compress current view state into a shareable URL. Options: { name?, description? } |
applyFromUrl() | Read #ctrlk= hash from current URL and apply the state. |
copyLink(options?) | Create link and copy to clipboard. |
peekLink(url) | Extract metadata from a shared URL without applying it. |
Tier 2 — Stored Sharing (app provides storage)
| Method | Description |
|---|---|
setProvider(provider) | Set storage backend: { save, load, list, delete } |
publish(name, options?) | Save view to shared store. Options: { scope?, expiresIn? } |
load(viewId) | Load a shared view by ID. Tracks usage count. |
list(options?) | List shared views. Sort by name, date, or useCount. |
remove(viewId) | Delete a shared view. |
createLocalProvider(ns?) | Built-in localStorage provider for testing. |
Tier 3 — Live Sharing (Enterprise)
| Method | Description |
|---|---|
startBroadcast() | Start broadcasting view state changes. |
stopBroadcast() | Stop broadcasting. |
follow(userId) | Follow another user's view in real-time. |
stopFollow() | Stop following. |
Events: share:link-created share:published share:loaded share:broadcast-start
React Integration Framework
Setup
import { CtrlKProvider } from '@ctrlk/react'; import ctrlk from 'ctrlk'; function App() { return ( <CtrlKProvider instance={ctrlk}> <YourApp /> </CtrlKProvider> ); }
Available Hooks
| Hook | Description |
|---|---|
useCtrlkCommand(def, deps) | Register a command. Auto-unregisters on unmount. |
useCtrlkView(key, default) | Two-way binding to a view state value. |
useCtrlkDensity() | Returns current density level. |
useCtrlkSelection() | Returns [selected, { select, deselect, toggle, clear }]. |
useCtrlkField(fieldId, options) | Register a form field for jump-to and tracking. |
useCtrlkShortcut(shortcut, handler) | Bind a shortcut inside a component. |
useCtrlk() | Access the raw CtrlK instance. |
import { useCtrlkCommand, useCtrlkView } from '@ctrlk/react'; function DataGrid() { // Command auto-registers, auto-unregisters on unmount useCtrlkCommand({ id: 'grid.refresh', title: 'Refresh Data', shortcut: 'Ctrl+R', execute: () => fetchData(), }, []); // View state survives navigation const [filters, setFilters] = useCtrlkView('grid.filters', {}); return <Grid filters={filters} />; }
Angular Integration Framework
Setup
import { CtrlkModule } from '@ctrlk/angular'; @NgModule({ imports: [CtrlkModule.forRoot()], })
Available Directives
| Directive | Description |
|---|---|
[ctrlkCommand] | Register a command from a template element. |
[ctrlkField] | Register a form field for jump-to navigation. |
[ctrlkShortcut] | Bind a shortcut to a component method. |
[ctrlkZone] | Define a focus navigation zone. |
<!-- Register commands from templates --> <button ctrlkCommand="export.csv" ctrlkCommandTitle="Export to CSV" ctrlkCommandShortcut="Ctrl+Shift+E" (click)="exportCsv()"> Export </button> <!-- Register fields for jump-to --> <input ctrlkField="patient.name" ctrlkFieldLabel="Patient Name" ctrlkFieldSection="Demographics" [(ngModel)]="patient.name" />
Vue 3 Integration Framework
import { onMounted, onUnmounted } from 'vue'; import ctrlk from 'ctrlk'; export function useCtrlkCommand(def) { let unregister; onMounted(() => { unregister = ctrlk.commands.register(def); }); onUnmounted(() => { unregister?.(); }); } export function useCtrlkField(fieldId, options) { onMounted(() => { ctrlk.fields.register(fieldId, options); }); }
AG Grid Adapter Adapter
import { AgGridAdapter } from '@ctrlk/ag-grid'; gridOptions.onGridReady = (params) => { const adapter = new AgGridAdapter(params.api); ctrlk.views.setGridAdapter(adapter); };
API Translation
| CtrlK Operation | AG Grid API Called |
|---|---|
| Capture state | gridApi.getColumnState() + getFilterModel() |
| Restore state | gridApi.applyColumnState() + setFilterModel() |
| Show/hide column | applyColumnState({ state: [{ colId, hide }] }) |
| Scroll to column | gridApi.ensureColumnVisible(colId) |
| Highlight column | gridApi.flashCells({ columns: [colId] }) |
| Set row height | updateGridOptions({ rowHeight }) + resetRowHeights() |
| Export CSV | gridApi.exportDataAsCsv() |
DevExtreme Adapter Adapter
const grid = new DevExpress.ui.dxDataGrid(el, { ... }); // DevExtreme has a built-in state() method const adapter = { captureState: () => grid.state(), restoreState: (s) => grid.state(s), setColumnVisible: (f, v) => grid.columnOption(f, 'visible', v), ensureColumnVisible: (f) => { const idx = grid.getVisibleColumnIndex(f); grid.getScrollable()?.scrollToElement(/*...*/); }, }; ctrlk.views.setGridAdapter(adapter);
Configuration
ctrlk.init({ // Feature toggles palette: true, // Enable command palette UI density: true, // Enable density controller autoDiscover: true, // Auto-register DOM elements session: true, // Enable session tracking macros: true, // Enable macro recording history: true, // Enable undo/redo // Shortcuts paletteShortcut: 'Ctrl+K', densityCycleShortcut: 'Ctrl+D', // ViewStateManager maxViews: 5, // Max saved views (LRU eviction) });
Events Reference
| Event | Module | Payload |
|---|---|---|
command:registered | CommandRegistry | { id, title } |
command:executed | CommandRegistry | { id, result } |
shortcut:fired | ShortcutEngine | { shortcut, commandId } |
palette:opened | CommandPalette | — |
palette:closed | CommandPalette | — |
density:changed | DensityController | { level, previous } |
view:saved | ViewStateManager | { name, slot, shortcut, remaining, evicted } |
view:loaded | ViewStateManager | { name } |
view:evicted | ViewStateManager | { name, reason, maxViews } |
selection:changed | SelectionModel | { selected, count } |
field:focused | FieldRegistry | { fieldId, label } |
session:reviewed | SessionTracker | { id, progress } |
macro:played | MacroEngine | { name, steps } |
history:undo | HistoryManager | { commandId } |
share:link-created | ViewShare | { name, length, compressedSize } |
share:published | ViewShare | { id, name, scope } |
ctrlk:initialized | Core | { version, options } |
Keyboard Shortcuts
Default Shortcuts (configurable)
| Shortcut | Action | Excel Equivalent |
|---|---|---|
Ctrl+K | Open command palette | — |
Ctrl+G | Jump to column / field | F5 / Ctrl+G |
Ctrl+D | Cycle density level | — |
Ctrl+/ | Show shortcut reference | — |
Ctrl+Z | Undo | Ctrl+Z |
Ctrl+Y | Redo | Ctrl+Y |
Ctrl+Shift+S | Share current view | — |
Ctrl+1 – Ctrl+5 | Load saved view by slot | — |
Alt+N | Next unreviewed / empty | — |
Alt+R | Toggle reviewed | — |
Ctrl+→ / Ctrl+← | Next / previous bookmark | Ctrl+Arrow |
F2 | Edit cell | F2 |
F6 | Next focus zone | F6 |
Escape | Close overlay / cancel | Escape |
ctrlk.keys.bind('your-shortcut', 'command.id').
CtrlK — The first IOUX for enterprise web apps · ctrlk.dev
MIT License · Prabhu Raja