GRIDKit is a zero-dependency PHP component framework designed for AI agents. 12 components, 1 CSS + 1 JS file, no build process. Your agent reads the skill file and builds complete CRUD applications in seconds.
Everything you need to build production-ready admin dashboards. Nothing you don't.
Built with AI agents in mind. Feed the skill file to your AI assistant and it generates complete GRIDKit applications — tables, forms, modals, authentication.
One CSS file. One JS file. No npm, no Composer, no build process. Clone and go. Works with any PHP 8.2+ project.
6 complete themes (Indigo, Ocean, Forest, Rose, Amber, Slate) with light and dark mode. All via CSS Custom Properties.
No page reloads. Tables search, sort, filter, paginate via AJAX. Forms submit and validate via AJAX. Everything stays fast.
Declarative, chainable API. Define a complete data table with search, sorting, pagination, and modals in under 15 lines of PHP.
Every component is mobile-ready. Tables switch to card layout, sidebars become overlays, forms reflow to single column.
Give your AI agent the GRIDKit skill file. It knows every component, every pattern, every best practice.
The GRIDKit Agent Skill is a structured document that teaches any AI assistant (Claude, GPT, Gemini, or any LLM) how to use GRIDKit optimally. It contains component references, code patterns, and best practices — everything an agent needs to generate correct GRIDKit code on the first try.
GRIDKIT_SKILL.md from the repositoryAdd this file to your AI agent's project context. It contains complete documentation for all 12 components, code patterns, JavaScript API reference, and common recipes.
You are building or maintaining a web application using GRIDKit, a lightweight PHP component framework for admin dashboards. This skill is the authoritative reference for correct GRIDKit usage.
GridKit\ | CSS prefix: gk- | Data attributes: data-gk-NEVER modify GRIDKit files inside consuming projects. Always change at the source first.
1. Edit source: /home/pawbot/projects/gridkit/ (= /home/develop/gridkit/) 2. Bump version: echo '1.4.x' > /home/pawbot/projects/gridkit/VERSION 3. Update: /home/develop/ssi-core/public/gridkit/VERSION (same if hardlinked) 4. Update CHANGELOG.md 5. Sync dashboard if needed (see Sync section)
GridKit\TableGridKit\FormGridKit\HeaderGridKit\SidebarGridKit\ModalGridKit\ButtonGridKit\AuthGridKit\ThemeGridKit\LayoutGridKit\StatCardsGridKit\FilterChipsGridKit\YearFilterGridKit\TableHeaderGridKit\LangGK.liveTableGridKit\BelegModalGridKit\ActionGroupIn SSI Panel, GRIDKit is loaded via the layouts/panel layout. Individual views just use components:
<?php
$this->layout('layouts/panel');
use GridKit\Table;
use GridKit\Form;
use GridKit\StatCards;
use GridKit\FilterChips;
use GridKit\Button;
?>
<?php $this->start('content') ?>
<!-- Your components here -->
<?php $this->end() ?>
// Static (client-side search/sort/pagination — for small datasets)
(new Table('my-table'))
->setData($rows) // array of assoc arrays
->search(['name', 'email']) // plain-text column keys only!
->toolbarHtml('<div class="gk-toolbar-spacer"></div>' . $addBtn)
->column('name', 'Name', ['sortable' => true])
->column('email', 'Email', ['sortable' => true])
->column('status', 'Status', ['format' => 'html']) // HTML column: not searched
->button('edit', ['icon' => 'edit', 'params' => ['id' => 'id']])
->button('delete', ['icon' => 'delete', 'params' => ['id' => 'id'], 'color' => 'danger'])
->paginate(25)
->render();
// Server-side (DB query)
(new Table('users'))
->query($db, "SELECT id, name, email, role FROM users ORDER BY name")
->search(['name', 'email'])
->column('name', 'Name', ['sortable' => true])
->column('email', 'Email', ['sortable' => true])
->column('role', 'Role', ['format' => 'label'])
->button('edit', ['icon' => 'edit', 'params' => ['id' => 'id']])
->paginate(25)
->render();
⚠️ Search rule: search() searches the column keys you name. If a column contains HTML (badges, links), use a separate plain-text key for search and a _display key for rendering. Never put HTML in searchable columns.
Column formats: currency, percent, date, datetime, boolean, label, html, email
Button colors: danger, success, warning, primary (default: neutral)
showIf: ->button('preview', ['icon' => 'open_in_new', 'params' => ['url' => 'url'], 'showIf' => 'has_preview'])
— button only renders if the row's has_preview value is truthy.
// Static render (returns HTML string)
Button::render('Label', [
'variant' => 'filled', // filled | outlined | tonal | text
'color' => 'primary', // primary | success | danger | warning | neutral
'icon' => 'add', // Material Icon name
'href' => '/path', // renders as <a>
'onclick' => 'jsCode()',
'size' => 'sm', // sm | md (default) | lg
]);
(new FilterChips('filter-id', 'status')) // 2nd param = GET param name
->baseUrl('/my-page')
->chip('', 'Alle (24)') // value='' = "All" chip (no GET param)
->chip('active', 'Aktiv (18)')
->chip('won', 'Gewonnen', ['color' => 'success'])
->chip('lost', 'Verloren', ['color' => 'danger'])
->preserve(['year']) // keep other GET params on click
->render();
Active chip is auto-detected from $_GET. Color options: success, danger, warning, primary.
$yf = new YearFilter('year-filter', 'year'); // 2nd param = GET param name
$yf->baseUrl('/my-page')
->range(2022, (int)date('Y')) // newest first
->preserve(['status'])
->render();
$currentYear = $yf->current(); // int — use for DB queries
The single source of truth for filter/search bars above tables. Three fixed sections in this exact order:
FilterChips like „All / Open / Paid")<details> for date / amount / detail filters)TableHeader::make('exp')
->status(fn() => $statusChips->render()) // closure
->search('q', $q, 'Search…', ['live' => 'exp-live']) // built-in
->filter(fn() => $yearFilter->render()) // closure
->filter('<select class="gk-filter">…</select>') // raw HTML
->advanced(fn() => renderDateRange(), 'Date & amount') // optional collapsible
->reset('/faktura/expenses') // optional reset btn
->render();
API:
make($id) static factorystatus(\Closure $renderer): top row, full widthsearch(string $name, string $value = '', string $placeholder = '…', array $opts = ['live' => '…', 'id' => '…'])filter($contentOrClosure): any number of toolbar slots — Closure (echo'd) or raw HTML stringadvanced(\Closure $renderer, string $summary = 'Erweiterte Filter', bool $open = false)reset(string $baseUrl, string $label = 'Filter zurücksetzen')CSS classes (all auto-applied): gk-tableheader, gk-tableheader-status, gk-tableheader-toolbar, gk-tableheader-advanced, gk-tableheader-spacer.
Do NOT build your own filter row with raw gk-toolbar / gk-toolbar-stacked if TableHeader fits — every table page must use this for visual consistency.
(new StatCards('stats-id'))
->card('Umsatz', 12450.80, ['format' => 'currency', 'icon' => 'euro', 'color' => 'primary'])
->card('Benutzer', 1284, ['format' => 'number', 'icon' => 'people', 'color' => 'success'])
->card('Fehler', 3, ['format' => 'number', 'icon' => 'error', 'color' => 'danger', 'highlight' => true])
->card('Quote', 78, ['format' => 'percent', 'icon' => 'speed', 'color' => 'warning'])
->card('Zu Details', '/url', ['icon' => 'arrow_forward', 'href' => '/url']) // clickable
->render();
Colors: primary, success, danger, warning, info
Formats: currency (1.234,56 €), number (1.234), percent (78 %)
// In layout (panel.php does this automatically):
<?php Modal::container(); ?>
// JS API — dynamic modal:
GK.modal.open('Titel', '<p>Beliebiges HTML</p>');
GK.modal.close();
// Static inline modal (for complex content):
<div class="gk-modal-overlay" id="my-modal" style="display:none;">
<div class="gk-modal gk-modal-small"> <!-- or gk-modal-large -->
<div class="gk-modal-header">
<h3 class="gk-modal-title">Titel</h3>
<button class="gk-modal-close"
onclick="document.getElementById('my-modal').style.display='none'">×</button>
</div>
<div class="gk-modal-body">Inhalt</div>
</div>
</div>
(new Form('user-form'))
->action('/api/save-user')
->method('POST')
->row()
->field('first_name', 'Vorname', 'text', ['width' => 8, 'required' => true])
->field('last_name', 'Nachname', 'text', ['width' => 8, 'required' => true])
->endRow()
->field('email', 'E-Mail', 'email', ['width' => 16])
->field('role', 'Rolle', 'select', ['width' => 8, 'options' => ['admin' => 'Admin', 'user' => 'User']])
->field('active', 'Aktiv', 'toggle')
->submit('Speichern')
->render();
Field types: text, textarea, select, number, date, time, email, tel, url, toggle, checkbox, radio, file, hidden, richtext, searchable-select
Form Density: Add gk-form-compact class to a <form> or wrapper <div> for compact forms. All elements scale down proportionally:
<!-- Normal --> <form>...</form> <!-- Compact --> <form class="gk-form-compact">...</form> <!-- As wrapper around multiple cards --> <div class="gk-form-compact"> <div class="gk-card">...</div> <div class="gk-card">...</div> </div>
Form endpoint must return JSON:
echo json_encode(['ok' => true]); // success echo json_encode(['ok' => true, 'message' => 'Saved!']); // with toast echo json_encode(['ok' => false, 'errors' => ['email' => 'Already exists']]); // validation
Theme::set('indigo', 'light'); // themes: indigo, ocean, forest, rose, amber, slate
Theme::switcher(); // renders theme-switcher UI
Lang::set('de'); // set locale (default: de)
Lang::jsConfig(); // MUST be output in <head> before gridkit.js — sets window.GK_LANG
// Toast notifications (use these exact forms!)
GK.toast.success('Gespeichert!');
GK.toast.error('Fehler aufgetreten!');
GK.toast.warning('Achtung!');
GK.toast.info('Information.');
// Dynamic modal
GK.modal.open('Titel', '<p>HTML-Inhalt</p>');
GK.modal.close();
// Table refresh (after save/delete in server-side mode)
GK.table.refresh('table-id');
GK.liveTable) — seit 1.9.0AJAX-gefilterte Tabellen: Search + Filter + Sort + Pagination ohne Full-Page-Reload.
Cursor bleibt beim Tippen, URL wird via history.replaceState synchron gehalten.
<!-- Inputs: beliebig ausserhalb des Containers -->
<input data-gk-live-input="my-tbl" name="q" placeholder="Suche">
<select data-gk-live-input="my-tbl" name="status">...</select>
<!-- Container: wird AJAX-getauscht -->
<div id="my-tbl" data-gk-live-table="/my-list">
<!-- Tabelle, Sort-Header (<a>), Paginierung — alles live -->
</div>
Controller-Seite: bei X-Requested-With: XMLHttpRequest oder ?partial=1 nur den Container-Inhalt ohne Layout rendern. Beispiel PHP:
if ($request->isAjax() || $request->get('partial') === '1') {
return $this->view('my-list-partial', $data);
}
return $this->view('my-list', $data);
Features:
<a href> innerhalb des Containers die auf denselben Endpoint zeigen → AJAX-Reload (Sort-Header, Pagination).patchNavSelects(): überschreibt onchange von <select data-gk-years> sodass sie window.location.search als Basis nehmen. Behält aktuelle Suche beim Jahr-Wechsel.gk-live-reloaded wird nach jedem Swap auf dem Container gefeuert — an Eigen-Code für Re-Init binden.// Sidebar mit AJAX-Navigation aktivieren $sidebar->ajaxNav(true);
<!-- Content-Container markieren --> <div class="gk-with-sidebar" data-gk-content> <!-- Dieser Bereich wird bei Navigation ersetzt --> </div>
Features:
gk-rootgk-with-sidebargk-body-with-headergk-btngk-btn-filledgk-btn-outlinedgk-btn-tonalgk-btn-textgk-btn-icon-onlygk-btn-smgk-cardgk-toolbar-spacergk-filter-chipsgk-chip gk-chip-activegk-stat-cardsgk-modal-overlaygk-modalgk-modal-small gk-modal-largegk-text-mutedgk-section-titlegk-page-headergk-emptyGlobaler PDF-/Dokument-Vorschau-Modal mit <iframe>. Eliminiert window.open() für PDF-Previews.
// Einmal pro Page (im Layout, vor </body>): \GridKit\BelegModal::container();
// JS-API überall:
GK.belegModal.open('/path/to/file.pdf');
GK.belegModal.open(url, { title: 'Rechnung 123' });
GK.belegModal.open(url, { autoPrint: true }); // druckt iframe nach load
GK.belegModal.open(url, {
unlinkExpenseId: 456, // zeigt "Verknüpfung trennen"-Btn
onUnlink: function() { location.reload(); }
});
GK.belegModal.close();
window.open(url) mit Console-Warning.Container für Action-Buttons in Tabellen-Spalten — vereinheitlicht das wiederkehrende
„flex-row mit Mini-Buttons"-Pattern. Eliminiert eigene .xx-btn-icon/.xx-btn-match Klassen.
// PHP-API (deklarativ):
\GridKit\ActionGroup::render([
['icon' => 'edit', 'onclick' => "edit($id)", 'title' => 'Bearbeiten'],
['icon' => 'delete', 'onclick' => "del($id)", 'title' => 'Löschen', 'color' => 'danger'],
['icon' => 'send', 'label' => 'Mahnen', 'color' => 'warning', 'variant' => 'filled',
'pill' => true, 'onclick' => "remind($id)", 'showIf' => $isOverdue],
]);
<!-- Oder rohes HTML (für JS-generierten Inhalt): -->
<div class="gk-action-group">
<button class="gk-btn gk-btn-xs gk-btn-text gk-btn-neutral gk-btn-icon-only">…</button>
<button class="gk-btn gk-btn-xs gk-btn-filled gk-btn-warning gk-btn-pill">…</button>
</div>
Neue CSS-Klassen:
.gk-action-group — inline-flex; gap:4px; flex-wrap:nowrap Container.gk-btn-xs — kleiner als gk-btn-sm (padding 3px 8px, font 11px). Icon-only: 26×26 px.gk-btn-pill — border-radius:999px (Badge-Stil)Action-Item-Optionen: icon, label, href, onclick, title, variant, color, size,
pill, disabled, showIf, class.
Tailwind-style utilities so consumers never need inline style="…" for spacing,
layout, typography, or semantic colors. Spacing scale: 0/1/2/3/4/5/6 = 0/4/8/12/16/20/24 px (MD3 8-grid with half-steps).
<!-- Don't: --> <div style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--gk-text-muted)">…</div> <!-- Do: --> <div class="gk-flex-center gk-gap-md gk-fs-md gk-text-muted">…</div>
/home/develop/gridkit/ = /home/pawbot/projects/gridkit/ — same inode, no sync needed.
Public assets served from /home/develop/ssi-core/public/gridkit/ — check if hardlinked too.
rsync -av /home/pawbot/projects/gridkit/ /home/pawbot/core/dashboard/gridkit/ \ --exclude='.git' --exclude='demo' --exclude='vendor' \ --exclude='src/Auth.php' --exclude='src/Sidebar.php' --exclude='js/gridkit.js'
⚠️ These 3 files have PawBot-specific extensions — NEVER overwrite:
src/Auth.php — Session-Lock, Remember-Cookiesrc/Sidebar.php — Brand-Image, HTML-Column supportjs/gridkit.js — Dashboard-specific extensionsDashboard is currently at v1.0.0 — needs sync!
search() column keys. Use plain-text key + separate display key.Lang::jsConfig() — "no_entries" shows as raw key. Must be in <head> before gridkit.js.gk-btn-filled not gk-btn--filled (no double dash).GK.toast.success() not GK.toast().GK.modal.open(title, html) takes title + HTML string, not an ID./home/pawbot/projects/gridkit/, never in consuming projects.Each component follows the same fluent PHP API. Chainable, declarative, zero boilerplate.
$table = new Table('products'); $table->query($db, "SELECT * FROM products ORDER BY name") ->search(['name', 'sku']) ->column('name', 'Product', ['sortable' => true]) ->column('sku', 'SKU', ['width' => '120px']) ->column('price', 'Price', ['format' => 'currency', 'sortable' => true]) ->column('is_active', 'Status', ['format' => 'label']) ->button('edit', ['icon' => 'edit', 'modal' => 'edit_product']) ->button('delete', ['icon' => 'delete', 'modal' => 'del', 'color' => 'error']) ->modal('edit_product', 'Edit', 'forms/product.php', ['size' => 'medium']) ->newButton('New Product', ['modal' => 'edit_product']) ->paginate(25) ->render();
Search, sort, paginate, AJAX reload, multi-select
16-column grid, 15 field types, AJAX submit
Groups, badges, collapse, mobile overlay
Fixed, search, user menu, theme switcher
Stackable dialogs, form-ready, sizes
KPI display with trends and colors
Session auth, bcrypt, remember-me
6 themes, light/dark mode
Filled, outlined, text, tonal, FAB
Sidebar-first, header-first modes
Clickable filter chip buttons
Year navigation filter
Clone, include, build. No configuration, no package managers, no build tools.
# Clone GRIDKit git clone https://github.com/mmollay/gridkit.git # Copy the skeleton as your starting point cp gridkit/skeleton.php my-app/index.php # That's it. Open in browser.