Funky Frame
The PSPWA JavaScript Framework
Building modern, secure, real-time web applications with vanilla JavaScript.
The Fine Print
This framework was built by a human developer and Claude Opus—sometimes seven of them depending on the task. Genius one minute, confidently wrong and clueless the next.
Zero Dependencies
No jQuery, no React, no framework tax. Just modern JavaScript using native browser APIs that didn't exist when jQuery was essential.
No Build Step
No Webpack. No Vite. No Babel. No "npm install" downloading half the internet. Edit a file, refresh the browser. Revolutionary.
80+ Components
Comprehensive UI component library: tables, forms, modals, toasts, wizards, trees, kanban boards, and much more.
12,000+ Browser Tests
"You can't test vanilla JavaScript" said nobody who actually tried. Our test runner executes in real browsers, not Node.js pretending to be one.
LLM Pair Programming
Claude debugged keyboard navigation until 3am. Seven Claudes wrote tests. Another spent a day fixing them. Claude also suggested deleting everything once.
Standalone or Integrated
Use as a complete framework or cherry-pick individual components. Works with any backend—originally extracted from a Perl/Mojolicious application.
What is a PSPWA?
PSPWA stands for Progressive Single Page Web Application. It's what happens when a PWA and an SPA have a baby, and that baby really cares about user experience.
The pattern of combining PWA capabilities with SPA architecture has been around for years. Gmail does it. Twitter does it. Most modern web apps worth their salt do it. We just gave it a name that's easier to Google.
The Family Tree
PWA Brings
- Offline support via Service Workers
- Install to home screen
- Push notifications
- Background sync
SPA Brings
- No full page reloads
- Smooth transitions
- Client-side routing
- Persistent state
Together They're Unstoppable
| Feature | PWA Only | SPA Only | PSPWA |
|---|---|---|---|
| Works offline | ✓ | ✗ | ✓ |
| Instant navigation | ✗ | ✓ | ✓ |
| Installable | ✓ | ✗ | ✓ |
| No page flicker | ✗ | ✓ | ✓ |
| Push notifications | ✓ | ✗ | ✓ |
| Background updates | ✓ | ✗ | ✓ |
| State persistence | ✗ | ✓ | ✓ |
Progressive Caching
Offline support is progressive—pages and API data are cached as you visit them. The more you explore, the more works offline.
Pro tip: Wander around while you have WiFi. Your future offline self will thank you.
Philosophy
Funky Frame is built on a set of core principles that guide every architectural decision:
1. Progressive Enhancement
The application works without JavaScript first. SPA behaviour is a layer, not a requirement. If JavaScript fails, users still have a fully functional application. This isn't just about graceful degradation—it's about resilience.
2. Modular Independence
Every component is a self-contained unit:
- No tight coupling — Components communicate through events, not direct references
- No global pollution — Everything lives under the
Funkynamespace - No dependency chains — Each module can be loaded independently
Bad: Tight coupling
TradesPage.table.reload();
Good: Event-based
Funky.PubSub.emit('funky:data:refresh', { entity: 'trades' });
3. Convention Over Configuration
Predictable patterns reduce cognitive load and accelerate development:
| Pattern | Convention |
|---|---|
| Page IDs | Derived from URL: /trades → trades |
| Entity types | Match API resource names: trade, client |
| API endpoints | Consistent: GET/POST/PUT/DELETE /api/{entity} |
| Lifecycle methods | Always: init(), destroy(), update() |
4. Cache-First Navigation
Once you've visited a page, returning to it is instant:
The cache is automatically invalidated when TTL expires, WebSocket signals an entity change, or user action modifies data.
5. Security by Default
Security isn't an afterthought—it's baked into every layer:
- CSRF protection on every state-changing request
- XSS prevention through input sanitization
- Secure module registration with Object.freeze
- Content Security Policy ready
Architecture Overview
The Frontend: FunkyJS
The Namespace
All JavaScript lives under the Funky global namespace:
window.Funky = {
// Core
Api: /* HTTP client */,
Events: /* DOM event utilities */,
PubSub: /* Application pub/sub messaging */,
Pages: /* Page lifecycle */,
SPA: /* Navigation */,
Storage: /* LocalStorage */,
WebSocket: /* Real-time connection */,
// Components
DataTables: /* Smart table wrapper */,
FormModal: /* JSON Schema forms */,
Toast: /* Notifications */,
Wizard: /* Multi-step forms */,
// Utilities
register: /* Module registration */,
has: /* Check if registered */,
};
Secure Registration
Modules are locked after registration—they cannot be overwritten:
Funky.register = function(name, module) {
if (Funky.has(name)) {
console.warn('[Funky] Module already registered:', name);
return false;
}
Object.defineProperty(Funky, name, {
value: Object.freeze(module),
writable: false,
configurable: false
});
return true;
};
This prevents malicious scripts from hijacking core modules.
Component Pattern
Every component follows the same structure:
(function() {
'use strict';
if (Funky.has('MyComponent')) return;
var MyComponent = {
// Configuration
config: { defaultOption: true },
// Initialization
init: function(options) {
this.config = Object.assign({}, this.config, options);
this.bindEvents();
},
// Cleanup
destroy: function() {
this.unbindEvents();
}
};
Funky.register('MyComponent', MyComponent);
})();
Security First
CSRF Protection
Every state-changing request requires a CSRF token:
// Funky.Api automatically handles this
Funky.Api.post('/api/trades', tradeData);
// Under the hood:
// 1. Reads token from csrf_token cookie
// 2. Adds X-CSRF-Token header
// 3. Server validates header matches cookie
// 4. Server rotates token after use
Module Protection
Core modules cannot be overwritten after registration:
// First registration succeeds
Funky.register('Api', myApiModule); // → true
// Subsequent attempts fail silently
Funky.register('Api', maliciousModule); // → false
// Console: [Funky] Module already registered: Api
XSS Prevention
Text content is escaped by default in DOM utilities:
// Safe - text is escaped
D.create('div').text(userInput);
// Only use html() with sanitized content
D.create('div').html(sanitizedHtml);
Accessibility
Every component is keyboard navigable, screen reader friendly, and respects motion preferences. We won't claim WCAG perfection—compliance is a moving target. But we've done the work, and we keep improving.
Try it now: Press Tab to navigate, or press F1 to see the keyboard shortcuts panel.
What We Actually Do
- Keyboard Navigation — Tab through everything, arrow keys in complex widgets, Esc to close modals. Focus trapped where it should be.
- Keyboard Shortcut Manager — Centralized registry via
Funky.Keyboard. Register shortcuts and they appear in the F1 panel automatically. - Semantic HTML — Proper <nav>, <main>, <section>, <aside>. Landmarks that screen readers can actually find.
- ARIA Labels — Icon buttons have labels. Live regions announce toasts. Modals are properly marked as dialogs.
- Focus Indicators — Clear 2px outlines using :focus-visible. No more "where did my focus go?"
- Reduced Motion — Respects prefers-reduced-motion AND has a manual toggle. Animations go to 0ms, not just "slower."
What Works Well
- Keyboard-only navigation
- Modal focus management
- Form labels on everything
- Theme contrast options
Still Wrestling With
- Complex data tables
- Screen reader edge cases
- Every ARIA role ever
Accessibility is built in, not bolted on. File an issue if something doesn't work with your assistive tech.
Real-Time Everything
WebSocket Connection
A persistent connection delivers updates instantly:
// Automatic connection and reconnection
Funky.WebSocket.connect();
// Subscribe to entity updates
Funky.PubSub.on('ws:entity_change', function(data) {
if (data.entity === 'trades' && data.action === 'created') {
Funky.Toast.success('New trade created!');
}
});
Server → Client Flow
Smart Updates
Pages declare which entities they care about:
var TradesPage = {
id: 'trades',
entities: ['trades', 'trade_allocations'], // Watch these
update: function(entity, id, action) {
// Called when WebSocket announces a change
this.table.ajax.reload(null, false);
}
};
Offline Queue
Work offline with confidence. The queue system captures actions when disconnected and syncs automatically when connectivity returns.
How It Works
User actions (creates, updates, deletes) are queued locally when offline. On reconnect, the queue processes automatically with retry logic and conflict resolution.
JobQueue
- Generic async job processor
- IndexedDB persistence
- Priority-based ordering
- Exponential backoff retry
RequestQueue
- HTTP-specific layer
- Wraps Funky.Api transparently
- Online/offline detection
- Conflict resolution strategies
Conflict Resolution
When offline edits conflict with server changes (HTTP 409), the system supports multiple resolution strategies:
- server-wins — Discard local changes
- client-wins — Force local changes with override header
- merge — Combine server and client data
- prompt — Ask the user to decide
The Page System
Lifecycle
Example Page
var ClientsPage = {
id: 'clients',
entities: ['clients', 'client_relationships'],
init: function(state) {
// Called when page becomes visible
this.initDataTable();
this.bindEvents();
// Restore scroll position from cached state
if (state && state.scrollY) {
window.scrollTo(0, state.scrollY);
}
},
destroy: function() {
// Called before navigating away
if (this.table) {
this.table.destroy();
}
// Return state to be cached
return { scrollY: window.scrollY };
},
update: function(entity, id, action) {
// Called when WebSocket signals change
if (this.table) {
this.table.ajax.reload(null, false);
}
}
};
Funky.Pages.register(ClientsPage);
Push Notifications
Browser Push with Service Worker
Funky supports native browser push notifications:
// Subscribe the user
Funky.Push.subscribe().then(function(subscription) {
console.log('Push subscription active');
});
Service Worker Handling
Notifications work even when the app is closed:
// In service worker
self.addEventListener('push', function(event) {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
data: data.data
})
);
});
// Click handler
self.addEventListener('notificationclick', function(event) {
event.notification.close();
event.waitUntil(
clients.openWindow('/trades/' + event.notification.data.trade_id)
);
});
Theming System
Three Built-in Themes
| Theme | Description |
|---|---|
| Dark (default) | Bloomberg/GitHub dark style — professional, easy on eyes |
| Light | Clean, bright — traditional enterprise feel |
| Even Funkier | Neon cyberpunk — because why not? |
CSS Variables
All theming uses CSS custom properties:
:root {
--pro-bg-primary: #0d1117;
--pro-bg-secondary: #161b22;
--pro-text-primary: #e6edf3;
--pro-accent-primary: #2f81f7;
/* ... 50+ variables ... */
}
[data-theme="light"] {
--pro-bg-primary: #ffffff;
--pro-text-primary: #1f2328;
}
[data-theme="even-funkyer"] {
--pro-bg-primary: #0a0015;
--pro-accent-primary: #ff00ff;
--pro-accent-secondary: #00ffff;
}
User Customization
Funky.Storage.set('user_preferences', {
theme: 'dark',
font_scale: 1.1,
accent_color: '#ff6b35',
compact_mode: true
});
Testing Framework
Funky includes a built-in JavaScript test runner that executes directly in the browser. No build tools, no Node.js dependency—just pure browser-based testing with real-time feedback.
Key Features
- Browser-Native — Tests run in the actual DOM environment, not a simulation
- Zero Dependencies — No npm, no bundlers, no transpilers required
- Real-Time Results — Watch tests pass/fail as they execute
- Component Isolation — Each test runs in a sandboxed iframe
- Async Support — Full Promise and callback support built-in
Writing Tests
Tests are defined using a simple, readable API:
Funky.Test.describe('Button Component', function(it) {
it('should emit click event', function(assert, done) {
var btn = D.create('button').text('Click Me');
E.on(btn.el, 'click', function() {
assert.ok(true, 'Click event fired');
done();
});
btn.el.click();
});
it('should add active class on focus', function(assert) {
var btn = D.one('.test-button');
btn.el.focus();
assert.ok(btn.hasClass('active'), 'Has active class');
});
});
Why Funky?
Compared to React/Vue/Angular
| Aspect | Funky | Modern SPA Frameworks |
|---|---|---|
| Bundle size | ~50KB JS | 200KB+ minimum |
| Build step | None required | Webpack/Vite/etc |
| Learning curve | Vanilla JS patterns | Framework-specific concepts |
| WebSocket | Built-in, automatic | Additional library needed |
| Offline support | Built-in queue system | Manual implementation |
| Test runner | Built-in browser tests | Jest/Vitest setup required |
When to Use Funky
Good fit: Enterprise/internal applications, data-heavy dashboards, real-time collaborative tools, teams who prefer vanilla JS
Consider alternatives: Public-facing marketing sites (static generators), mobile-first applications (React Native, Flutter), micro-frontends architecture