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\LangIn 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
(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');
// 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-empty/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.