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

Diagram showing PSPWA evolution

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 comparison between PWA Only, SPA Only, and PSPWA
FeaturePWA OnlySPA OnlyPSPWA
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.

Diagram showing progressive caching flow

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.

Diagram showing progressive enhancement

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 Funky namespace
  • 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:

Funky framework conventions
PatternConvention
Page IDsDerived from URL: /tradestrades
Entity typesMatch API resource names: trade, client
API endpointsConsistent: GET/POST/PUT/DELETE /api/{entity}
Lifecycle methodsAlways: init(), destroy(), update()

4. Cache-First Navigation

Once you've visited a page, returning to it is instant:

Diagram showing cache-first navigation

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

Client-side architecture diagram showing FunkyJS core modules, components, and communication with any backend

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

Real-time data flow diagram

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

Page lifecycle diagram showing states

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

Available theme options
ThemeDescription
Dark (default)Bloomberg/GitHub dark style — professional, easy on eyes
LightClean, bright — traditional enterprise feel
Even FunkierNeon 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

Funky compared to modern SPA frameworks
AspectFunkyModern SPA Frameworks
Bundle size~50KB JS200KB+ minimum
Build stepNone requiredWebpack/Vite/etc
Learning curveVanilla JS patternsFramework-specific concepts
WebSocketBuilt-in, automaticAdditional library needed
Offline supportBuilt-in queue systemManual implementation
Test runnerBuilt-in browser testsJest/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

"Write less code. Ship more features. Sleep better at night."

Funky Frame eliminates boilerplate by providing sensible defaults while remaining flexible enough for complex requirements. Real-time updates, offline support, and accessibility come built-in—you just build your features.