Step-by-step instructions for integrating CtrlK into five different application architectures. Each section maps to a live demo you can run and inspect.
Pattern A drop-in. Add CtrlK to any existing page with zero build step. Shows column profiles, density control, view sharing.
Add the single script tag. CtrlK auto-initializes, scans the DOM, and registers all buttons and links as commands.
<!-- Drop this before </body>. Nothing else needed. --> <script src="ctrlk.runtime.js"></script> <!-- Ctrl+K now opens command palette automatically. --> <!-- All <button> elements are auto-discovered as commands. -->
Go beyond auto-discovery. Register named commands with shortcuts, categories, and execute functions.
// Register a command — appears in Ctrl+K palette ctrlk.commands.register({ id: 'view.engineering', title: 'Show Engineering Department', category: 'Department Views', icon: '⚙', shortcut: 'Alt+1', execute: () => filterByDept('Engineering'), }); // Bind the shortcut ctrlk.keys.bind('Alt+1', 'view.engineering');
Define which columns to show for different tasks. Users switch profiles from the palette or via shortcuts.
// Define column profiles const PROFILES = { people: ['empId','name','department','title','status','location'], compensation: ['empId','name','band','salary','tenure'], contact: ['empId','name','email','phone','deskId'], }; function showColumns(profileName) { const cols = PROFILES[profileName]; // Hide all columns, show only these allColumns.forEach(col => { col.visible = cols.includes(col.id); }); renderGrid(); } ctrlk.commands.register({ id: 'cols.people', title: 'Columns: People Overview', category: 'Column Profiles', icon: '👤', execute: () => showColumns('people'), });
Build a column search overlay. On selection, scroll the grid horizontally to the column and highlight it.
// Get all columns for the search index const searchableColumns = COLUMNS.map((col, i) => ({ id: col.id, name: col.headerName, index: i, })); // Search function — filter by query function searchColumns(query) { const q = query.toLowerCase(); return searchableColumns.filter(c => c.name.toLowerCase().includes(q) ); } // Jump to column — scroll + highlight function jumpToColumn(col) { const gridWrap = document.querySelector('.grid-wrap'); const header = document.querySelectorAll('th')[col.index]; // Scroll horizontally to center the column const scrollLeft = header.offsetLeft - gridWrap.offsetWidth / 2; gridWrap.scrollTo({ left: scrollLeft, behavior: 'smooth' }); // Flash the column header.classList.add('col-highlight'); setTimeout(() => header.classList.remove('col-highlight'), 1500); } // Register Ctrl+G shortcut ctrlk.keys.bind('Ctrl+G', 'ctrlk.colsearch');
Capture the current view state (filters, columns, sort) and encode it into a shareable URL.
function shareCurrentView() { const state = { filter: currentFilter, sort: sortColumn, columns: visibleColumns, selected: [...selectedRows], }; const encoded = btoa(JSON.stringify(state)); const link = location.href.split('#')[0] + '#ctrlk=' + encoded; navigator.clipboard.writeText(link); } // On page load — check for shared view in URL if (location.hash.startsWith('#ctrlk=')) { const state = JSON.parse(atob(location.hash.slice(7))); applyViewState(state); }
Pattern C deep integration using React hooks. Shows session tracking with batch review progress, selection with batch actions.
npm install ctrlk
import { CtrlKProvider } from '@ctrlk/react'; import ctrlk from 'ctrlk'; function App() { return ( <CtrlKProvider instance={ctrlk}> <TicketQueue /> </CtrlKProvider> ); }
Commands auto-unregister on component unmount. The hook wraps the latest closure, so execute always has fresh state.
import { useCtrlkCommand } from '@ctrlk/react'; function TicketQueue() { const [view, setView] = useState('all'); // This command auto-registers and auto-unregisters useCtrlkCommand({ id: 'view.open', title: 'Show Open Tickets', category: 'Views', icon: '🟢', execute: () => setView('open'), }, []); // empty deps = register once }
Track which tickets have been reviewed. Show progress. Navigate to next unreviewed with Alt+N.
const [reviewed, setReviewed] = useState(new Set()); // Toggle reviewed status (Alt+R) function toggleReviewed(ticketId) { setReviewed(prev => { const next = new Set(prev); next.has(ticketId) ? next.delete(ticketId) : next.add(ticketId); return next; }); } // Next unreviewed (Alt+N) function nextUnreviewed() { const next = filteredTickets.find(t => !reviewed.has(t.id)); if (next) { setFocused(next.id); document.querySelector(`[data-ticket="${next.id}"]`) ?.scrollIntoView({ behavior: 'smooth' }); } } // Progress bar data const progress = { done: filteredTickets.filter(t => reviewed.has(t.id)).length, total: filteredTickets.length, }; // Register as commands useCtrlkCommand({ id:'review.toggle', shortcut:'Alt+R', execute: toggleReviewed }); useCtrlkCommand({ id:'review.next', shortcut:'Alt+N', execute: nextUnreviewed });
// Green left border = reviewed // Blue background = selected <div className={`ticket-row ${selected.has(ticket.id) ? 'selected' : ''} ${reviewed.has(ticket.id) ? 'reviewed' : ''}`} data-ticket={ticket.id} onDoubleClick={() => toggleReviewed(ticket.id)} > ... </div>
Field-level CtrlK integration. Shows jump-to-field, empty field navigation, dirty tracking, field pinning, and completeness.
Structure fields into logical sections. Each field has a label, section, and editability flag.
const SECTIONS = [ { id: 'vitals', title: 'Vitals', icon: '❤️', fields: ['bloodPressure', 'heartRate', 'temperature', 'weight', 'height', 'bmi', 'oxygenSat'], }, // ... more sections ]; const LABELS = { bloodPressure: 'Blood Pressure', heartRate: 'Heart Rate (bpm)', // ... one label per field }; const READONLY = new Set(['mrn', 'ssn', 'bmi']); const REQUIRED = new Set(['firstName', 'bloodPressure']);
Unlike grid column search, field search operates on form fields. Shows section, empty/dirty status, and pin state.
function searchFields(query) { const q = query.toLowerCase(); const results = []; for (const section of SECTIONS) { for (const fieldId of section.fields) { const label = LABELS[fieldId]; if (label.toLowerCase().includes(q)) { results.push({ field: fieldId, label: label, section: section.title, empty: isEmpty(fieldId), dirty: isDirty(fieldId), pinned: pinnedFields.has(fieldId), }); } } } return results; } // Jump to field — scroll, highlight, focus input function scrollToField(fieldId) { const el = document.querySelector(`[data-field="${fieldId}"]`); el?.scrollIntoView({ behavior: 'smooth', block: 'center' }); const input = el?.querySelector('input'); input?.focus(); }
// Track original values for dirty comparison const data = ref({...PATIENT}); const original = ref({...PATIENT}); // Computed: which fields changed? const dirtyFields = computed(() => { return Object.keys(data.value) .filter(k => data.value[k] !== original.value[k]) .map(k => ({ field: k, label: LABELS[k], old: original.value[k], new: data.value[k], })); }); // Show diff modal with Ctrl+Shift+D // Yellow border on dirty inputs via :class binding <input :class="{dirty: isDirty(fieldId)}" v-model="data[fieldId]" />
const pinnedFields = ref(new Set()); function togglePin(fieldId) { const p = new Set(pinnedFields.value); p.has(fieldId) ? p.delete(fieldId) : p.add(fieldId); pinnedFields.value = p; } <!-- Pinned bar at top of page --> <div class="pinned-bar" v-if="pinnedFields.size > 0"> <div v-for="f in pinnedFields" class="pinned-item"> {{ LABELS[f] }}: {{ data[f] }} </div> </div>
CtrlK + AG Grid Community. Shows the GridAdapter in action — view state capture/restore, column search via grid API, density affecting row height.
import { AgGridAdapter } from '@ctrlk/ag-grid'; // After AG Grid is ready const gridOptions = { columnDefs: columnDefs, rowData: products, onGridReady: (params) => { const adapter = new AgGridAdapter(params.api, { rowIdField: 'sku', }); // Wire CtrlK to AG Grid ctrlk.connectGrid(adapter); // This single call connects: // → views (capture/restore column state) // → selection (sync selected rows) // → columnNav (search via AG Grid API) }, };
ctrlk.connectGrid(adapter) wires views, selection, and column navigation to the grid simultaneously. No per-module configuration needed.The column search overlay reads from gridApi.getColumnState() and jumps using gridApi.ensureColumnVisible().
// Column search reads from AG Grid API function getColumns() { return gridApi.getColumnState().map(cs => ({ colId: cs.colId, name: columnDefs.find(cd => cd.field === cs.colId)?.headerName, hidden: cs.hide, })); } // Jump uses AG Grid's native scroll + flash function jumpToColumn(colId) { // Make visible if hidden if (col.hidden) { gridApi.applyColumnState({ state: [{ colId: colId, hide: false }] }); } // Scroll to it gridApi.ensureColumnVisible(colId); // Flash highlight gridApi.flashCells({ columns: [colId] }); }
const densityConfig = { compact: { rowHeight: 28, headerHeight: 30 }, comfortable: { rowHeight: 36, headerHeight: 36 }, spacious: { rowHeight: 48, headerHeight: 42 }, }; function setDensity(level) { const cfg = densityConfig[level]; // AG Grid API for row height gridApi.updateGridOptions({ rowHeight: cfg.rowHeight, headerHeight: cfg.headerHeight, }); gridApi.resetRowHeights(); }
// Save: capture AG Grid's full state function saveView(name) { const state = { columns: gridApi.getColumnState(), filters: gridApi.getFilterModel(), sort: gridApi.getColumnState() .filter(c => c.sort) .map(c => ({ colId: c.colId, sort: c.sort })), }; localStorage.setItem(`view-${name}`, JSON.stringify(state)); } // Restore: apply everything back function loadView(name) { const state = JSON.parse(localStorage.getItem(`view-${name}`)); gridApi.applyColumnState({ state: state.columns, applyOrder: true }); gridApi.setFilterModel(state.filters); }
Same app as 4A, different grid library. Shows the adapter abstraction — same CtrlK commands, different API calls underneath.
Every CtrlK operation maps to a different DevExtreme API call. The user experience is identical.
| CtrlK Operation | AG Grid API | DevExtreme API |
|---|---|---|
| Get column state | gridApi.getColumnState() | dxGrid.state() |
| Restore state | gridApi.applyColumnState() | dxGrid.state(saved) |
| Show/hide column | applyColumnState({hide}) | columnOption(field,'visible') |
| Scroll to column | ensureColumnVisible() | getScrollable().scrollToElement() |
| Flash cells | flashCells({columns}) | CSS animation (manual) |
| Set filter | setFilterModel() | filter(['field','=','val']) |
| Clear filters | setFilterModel(null) | clearFilter() |
| Select all | selectAllFiltered() | selectAll() |
| Export CSV | exportDataAsCsv() | exportToExcel() |
| Row height | updateGridOptions({rowHeight}) | CSS padding on cells |
// DevExtreme has a built-in state() method // It captures EVERYTHING — columns, filters, sort, grouping function saveView(name) { const state = dxGrid.state(); // One call. Done. localStorage.setItem(`view-${name}`, JSON.stringify(state)); } function loadView(name) { const state = JSON.parse(localStorage.getItem(`view-${name}`)); dxGrid.state(state); // One call. Everything restored. } // Column visibility toggle — different API than AG Grid function showOnlyColumns(fieldNames) { ALL_COLUMNS.forEach(col => { dxGrid.columnOption( col.dataField, 'visible', fieldNames.includes(col.dataField) ); }); }
These shortcuts work across all demos. They follow Excel conventions where applicable.
| Shortcut | Action | Demos | Excel Equivalent |
|---|---|---|---|
| Ctrl+K | Open command palette | All | — |
| Ctrl+G | Jump to column / field | All | F5 / Ctrl+G (Go To) |
| Ctrl+D | Cycle density | All | — |
| Ctrl+/ | Show all shortcuts | All | — |
| Ctrl+Shift+S | Share current view | 1, 4A, 4B | — |
| Alt+N | Next empty / unreviewed | 2, 3 | — |
| Alt+R | Toggle reviewed | 2 | — |
| Ctrl+Shift+D | Show dirty diff | 3 | — |
| Ctrl+S | Save changes | 3 | Ctrl+S |
| Ctrl+1-4 | Saved views | 4A, 4B | — |
| Alt+1-5 | Quick department views | 1 | — |
| Ctrl+←→ | Jump between bookmarks | 1 | Ctrl+Arrow (boundaries) |
| F2 | Edit cell / field | Core module | F2 |
| F6 | Cycle between zones | Core module | F6 (panes) |
| Escape | Close overlay / cancel | All | Escape |
User Input (keyboard, mouse) │ ▼ ┌──────────────────────────┐ │ CtrlK Runtime │ ◄── The IOUX layer │ │ │ CommandRegistry │ All actions are commands │ ShortcutEngine │ Scope-aware key binding │ CommandPalette │ Ctrl+K search UI │ ViewStateManager │ Named views + auto-save │ SelectionModel │ Cross-page persistent sets │ FieldRegistry │ Field nav + dirty tracking │ ColumnNavigator │ Column search + bookmarks │ SessionTracker │ Batch review progress │ MacroEngine │ Record + replay │ HistoryManager │ Undo/redo + branching │ ViewShare │ Shareable view links │ DensityController │ Compact/comfortable/spacious └──────────┬───────────────┘ │ ▼ ┌──────────────────────────┐ │ Grid Adapter │ ◄── Translates to grid API │ │ │ @ctrlk/ag-grid │ AG Grid v28-31+ │ @ctrlk/devextreme │ DevExtreme DataGrid │ @ctrlk/kendo │ Kendo UI Grid │ @ctrlk/generic │ Plain HTML tables └──────────┬───────────────┘ │ ▼ ┌──────────────────────────┐ │ Your Application │ ◄── No changes needed │ │ │ React / Angular / Vue │ Framework components │ AG Grid / DevExtreme │ Grid library │ Bootstrap / Material │ CSS framework └──────────────────────────┘