Part 3 of 4: Architectural Evolution in Horde 6
I really did not want to build another frontend framework. Or adopt one for that matter.
When jQuery Mobile became untenable (see Part 1), my instinct was to find the next framework. Something modern, well-maintained, with good documentation and community support. React? I embraced it wholeheartedly initially and it worked well for other projects. But integrating it with Horde as-is was some kind of tainted love. Too heavy, too much suffocating our server-rendered PHP. I had been there before in my actual job. Vue? Same problem, same story. Framework7? Smaller community, uncertain longevity. Do I really want to bet the farm on that?
I think I presented some of these arguments in part 1 already.
Every option had the same problem lurking beneath the surface: I’d be signing up for the next deprecation cycle. In five years I’d be having this exact conversation again.
“Framework X was great but now everyone’s moved on to Framework Y.”
Simply can’t rewrite the frontend every five years.
The revelation came slowly and then all at once: Modern web standards are good enough. Horde won’t need a framework. CSS Grid, Flexbox, CSS Variables, native modern JavaScript. These technologies are mature, stable, and most importantly they aren’t going away. Browser vendors maintain backwards compatibility because billions of websites depend on these standards.
This is the economic argument for vanilla web development in volunteer projects. It’s not about being a purist or avoiding popular tools out of spite. It’s about sustainability over convenience. It’s about making architectural choices we can actually maintain long-term without burning out the few volunteers we have.
The Framework Fatigue Problem
Did we already talk about the frameworks Horde has used over its 25 year history?
2000-2005: Plain HTML, tables for layout, inline styles 2005 onwards: PrototypeJS + Scriptaculous for desktop AJAX2012-2021: jQuery Mobile for mobile interface 2021-2026: PrototypeJS still on desktop, jQuery Mobile deprecated, searching for replacement
Every framework transition was painful: – Rewrite working code – Learn new patterns and APIs – Update documentation – Assist contributors – Test across browsers – Fix regressions
And every framework eventually became legacy:
PrototypeJS: Peak popularity 2006-2010. Eclipsed by jQuery. Barely maintained these days. Still works, but no new features. Archaic in approach and much of the code base solves problems we last had 15 years ago.
jQuery Mobile: Peak popularity 2012-2014. Deprecated 2021. Unmaintained. Security concerns. Browser compatibility issues.
The pattern is clear: every framework has a lifecycle. Adoption, growth, maturity, decline, deprecation. The question isn’t “Will this framework be deprecated?” It’s “When?”
For commercial software vendors framework migrations are an operational expense. You budget developer time, plan the migration, execute it, move on.
For volunteer open source projects, framework migrations are an existential risk. If key contributors burn out during migration or simply don’t like the new stack the project stalls. If the migration takes too long the project will lose users. If the migration introduces bugs, trust erodes.
We needed to break this cycle.
What “Modern Web Standards” Actually Means
When I say “vanilla web development”, I don’t mean going back to 1999. Modern web standards are incredibly powerful:
CSS Grid (2017)
Handles complex layouts that used to require framework grid systems:
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}This creates a responsive grid. It automatically wraps to rows on
narrow screens. No media queries needed. No framework classes like
col-md-4 or grid-a. The browser handles it
all.
Browser support: 98%+ (everything except IE11 which is dead, Jim! Microsoft ended support for in 2022).
CSS Flexbox (2015)
Handles most layout needs—centering, alignment, distribution:
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}That’s it. Three lines for a toolbar with items spaced evenly and vertically centered. No framework helper classes, no JavaScript calculation.
Browser support: 99%+ (universal).
CSS Variables (2016)
Runtime theming without preprocessors or build tools:
:root {
--color-primary: #0066cc;
--color-bg: #ffffff;
--space-md: 1rem;
}
.button {
background: var(--color-primary);
color: var(--color-bg);
padding: var(--space-md);
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #4488ff;
--color-bg: #1a1a1a;
}
}Change the variables, entire UI updates instantly. No SASS compilation, no CSS-in-JS runtime, no webpack. Just native CSS.
Browser support: 98%+ Everything except IE11. Still dead. There won’t be a Genesis for you, Internet Explorer!
CSS @layer (2022)
Manage specificity and style precedence:
@layer reset, base, components, utilities;
@layer base {
body { font-family: sans-serif; }
}
@layer components {
.button { background: blue; }
}
/* Utilities always win, even though they're defined first */
@layer utilities {
.bg-red { background: red !important; }
}This solves the CSS specificity nightmare that frameworks like Tailwind or BEM conventions tried to address. But it’s native CSS, no framework needed.
Browser support: 90%+ Sorry dear missing Safari < 15.4 but it’s not too bad. It degrades gracefully.
Native HTML5 Elements
Modern HTML provides widgets that used to require JavaScript:
<details>
and <summary>: Collapsible sections, no
JavaScript
<details>
<summary>Advanced Options</summary>
<div>Content here expands/collapses automatically</div>
</details><dialog>:
Modal dialogs, native browser behavior
<dialog id="confirm">
<p>Are you sure?</p>
<button onclick="this.closest('dialog').close('yes')">Yes</button>
<button onclick="this.closest('dialog').close('no')">No</button>
</dialog>
<script>
document.querySelector('#confirm').showModal();
</script><progress>
and <meter>: Progress bars and
gauges
<progress value="70" max="100">70%</progress>
<meter value="0.7" min="0" max="1">70%</meter>All of these are fully accessible (ARIA attributes built in), keyboard navigable, and work without JavaScript.
Browser support: 97%+ for
<details>, 97%+ for <dialog>, 99%+
for <progress>.
The Theme System: 2KB vs 5MB
Let me show you the economic impact of vanilla CSS with a concrete example. Let’s look into theming.
jQuery Mobile Themes (What We Had)
Each jQuery Mobile theme required: – Base CSS: ~115 KB – Theme-specific CSS: ~115 KB – Icons: 743 PNG files (~5-10 MB per theme) – JavaScript for theme switching: ~20 KB
Total per theme: ~10 MB
Supporting 3 themes (blue, silver, red): ~30 MB of assets.
Theme switching required: 1. Change CSS file reference 2. Reload entire page (to load new CSS) 3. Download new icon set 4. Re-initialize jQuery Mobile widgets
Slow. Heavy. Resource eater.
Vanilla CSS Themes (What We Built)
Each vanilla CSS theme requires: – Token file (CSS variables): ~2-5 KB – Shared base CSS (one file for all themes): ~15 KB – Icons: Single SVG sprite sheet (~100 KB, shared across all themes)
Total per theme: ~2-5 KB (plus one-time 115 KB for base CSS + icons)
Supporting 3 themes: ~15 KB + 115 KB shared = ~130 KB total.
Theme switching: 1. Load new token file (2-5 KB) 2. CSS variables update instantly 3. No page reload, no JavaScript
Size reduction: 98% (30 MB → 130 KB)
How It Works
Base styles use CSS variables:
/* shared/base.css - loaded once */
.button {
background: var(--color-primary);
border: 1px solid var(--color-border);
color: var(--color-text);
padding: var(--space-md);
border-radius: var(--border-radius);
font-size: var(--font-size-base);
}
.button:hover {
background: var(--color-primary-dark);
}
.button:focus {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}Theme files define variables:
/* themes/blue/tokens.css */
:root {
--color-primary: #0066cc;
--color-primary-dark: #004c99;
--color-bg: #ffffff;
--color-text: #333333;
--color-border: #dddddd;
--color-focus: #0066cc;
--space-md: 1rem;
--border-radius: 4px;
--font-size-base: 1rem;
}/* themes/dark/tokens.css */
:root {
--color-primary: #4488ff;
--color-primary-dark: #6699ff;
--color-bg: #1a1a1a;
--color-text: #e0e0e0;
--color-border: #333333;
--color-focus: #4488ff;
--space-md: 1rem;
--border-radius: 4px;
--font-size-base: 1rem;
}Switching themes:
// No framework, 5 lines
function switchTheme(themeName) {
const link = document.querySelector('link[data-theme]');
link.href = `/themes/${themeName}/tokens.css`;
localStorage.setItem('theme', themeName);
}The browser re-evaluates CSS variables automatically. Every element
using var(--color-primary) updates instantly. No page
reload, no FOUC (flash of unstyled content), no JavaScript
recalculation.
Automatic Dark Mode
Bonus: CSS variables enable automatic dark mode:
/* themes/blue/tokens.css */
:root {
--color-primary: #0066cc;
--color-bg: #ffffff;
--color-text: #333333;
}
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #4488ff;
--color-bg: #1a1a1a;
--color-text: #e0e0e0;
}
}Users on macOS, iOS, Android, or Windows with system dark mode enabled automatically get dark theme if done right. No settings screen, no manual toggle. It just works. Caveat: It doesn’t. Not completely, not across the visible barrier between renovated parts and those which we try to make similar but deliver using old technology still.
This is a better user experience than frameworks provide—because it’s native browser behavior.
No Build Tools
Here’s what we don’t need:
- No webpack: CSS and JavaScript are hand-written,
served directly - No SASS/LESS: CSS variables handle theming, native
CSS handles nesting (via:is()and:where())
all make us not need or want SASS right now. In case we notice we want
it, we can re-adopt it. I’d rather not though. - No PostCSS: Modern CSS doesn’t need autoprefixer
anymore (browser support is universal) - No Babel: We write ES6+ JavaScript, modern browsers
support it natively - No minifier: HTTP compression (gzip/brotli) handles
file size - No bundler: HTTP/2 makes multiple small files
efficient
This has profound implications for maintenance:
No build step means: – Clone repo, edit CSS, reload browser. View changes immediate in your developer install. – No npm install step (1000+ dependencies for a typical frontend build) – No version conflicts between Node.js, webpack, Babel, PostCSS – No “it works on my machine” build environment issues. At least not for presentation reasons. – No CI/CD complexity for building assets. We have enough CI/CD complexity otherwise and don’t need any on top. – No source maps debugging. The source code is the deployed code.
For casual contributors, this is huge. Want to fix a CSS bug? Edit the file, test in browser, submit PR. No build setup, no toolchain learning curve. Do it like they did when the web was young but with a power we didn’t have back then.
For long-term maintenance, this is essential. In 2030, the CSS we write today will still work. No webpack upgrade treadmill, no Babel configuration debugging, no dependency security audits for 1000+ npm packages.
The Economic Argument for Volunteer Projects
Let me be explicit about the economics.
Commercial web development optimizes for developer velocity. How quickly can we ship features? Frameworks provide some of that velocity. Component libraries, tooling, conventions. The maintenance burden (framework updates, migration costs) is an accepted operational expense.
Volunteer open source projects optimize for sustainability. Can we maintain this code with limited contributor time? Frameworks can be liabilities in this aspect. They require learning, maintenance, eventual migration. They have their own life cycle application developers cannot mitigate. The velocity benefit doesn’t offset the long-term burden.
Time Budget Reality
Horde has a limited set of active contributors. Some do few hours per week. Some do huge pieces of development in a stretch but need to go dark for months at the most inconvenient time. All shared across 150+ component repositories. Around 200 if you count supporting and legacy pieces.
A framework migration costs: – Learning time: 10-20 hours minimum per contributor, easily multiple times that! – Migration execution: 80-160 hours (rewriting templates, testing) – Bug fixing: 50-200 hours (regressions, edge cases) – Documentation: Hours we probably won’t have. Admittedly it’s not good but documentation is what gives first.
Total: Hundreds of hours for a single migration every few years.
That’s months of total available contributor time spent on migration instead of features and bug fixes. Let alone user support.
If framework lifecycles are 5-8 years, we spend 20-40% of our time on migrations rather than actual software development.
Vanilla web development cracks down on this quite a bit. Web standards don’t deprecate fast if at all. CSS Grid from 2017 works identically in 2026. It will work in 2035. We might choose to do something different by then but nobody will force our hands.
The upfront cost is a bit higher. Our component library and ready-made widgets are what the browser offers out of the box. Everything beyond that must be crafted. But the long-term maintenance burden is far lower.
For volunteer projects, low maintenance burden beats high initial velocity.
What We Actually Built
The proof is in the implementation. Here’s what vanilla web development looks like in practice:
Responsive Login
File structure:
/templates/login/
login.html.php (5 KB - plain HTML5)
login.css (3 KB - vanilla CSS)
login.js (2 KB - form validation)
/themes/
default/tokens.css (2 KB - Tie in to the blue/teal desktop theme inherited from H5)
dark/tokens.css (2 KB - Not counting some palette swapped PNGs)
red/tokens.css (2 KB - Another palette swap to tie into https://dev.horde.org look)
/shared/
base.css (15 KB - shared styles)
icons.svg (100 KB - sprite sheet)Total: ~130 KB for the touched-up login system including 3 themes. Much of this reusable for the actual application pages.
HTML (simplified):
<div class="login-container">
<form method="post" class="login-form">
<h1>Horde Login</h1>
<div class="form-field">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-field">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="button-primary">Log In</button>
</form>
</div>Plain HTML5. No framework directives, no data-binding, no component props. Just semantic HTML.
CSS:
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: var(--color-bg);
}
.login-form {
width: 100%;
max-width: 400px;
padding: var(--space-lg);
background: var(--color-surface);
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
}
.form-field {
margin-bottom: var(--space-md);
}
.form-field label {
display: block;
margin-bottom: var(--space-sm);
color: var(--color-text);
font-weight: 500;
}
.form-field input {
width: 100%;
padding: var(--space-md);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
font-size: var(--font-size-base);
}
.button-primary {
width: 100%;
padding: var(--space-md);
background: var(--color-primary);
color: var(--color-bg);
border: none;
border-radius: var(--border-radius);
font-size: var(--font-size-base);
cursor: pointer;
}
/* Responsive: no media queries needed for basic layout */
/* Flexbox handles centering on all screen sizes */No framework classes. No btn btn-primary btn-lg. Just
semantic class names using CSS variables.
JavaScript (form validation):
document.querySelector('.login-form').addEventListener('submit', (e) => {
const username = document.querySelector('#username').value.trim();
const password = document.querySelector('#password').value;
if (!username || !password) {
e.preventDefault();
alert('Please enter username and password');
return;
}
// Form submits normally (server-side validation is primary)
});10 lines. No framework. Just vanilla JavaScript.
Mobile Portal
Grid layout:
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-md);
padding: var(--space-md);
}
.app-card {
padding: var(--space-md);
background: var(--color-surface);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.app-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.app-icon {
width: 48px;
height: 48px;
margin-bottom: var(--space-sm);
}
.app-name {
font-weight: 600;
color: var(--color-text);
}Responsive grid that automatically adjusts columns based on available width. No JavaScript, no framework grid system, no media queries. The browser handles it.
Performance Benefits
Vanilla web development has measurable performance benefits:
Download size: 70 KB (vanilla) vs 488 KB (jQuery Mobile) = 85% smaller
Parse time: No framework JavaScript to parse and execute. Browser executes ~25 KB of vanilla JS vs ~221 KB of jQuery + jQuery Mobile. A ReactJs prototype approached similar size ranges easily.
Initial render: CSS variables are evaluated once, then cached. No runtime CSS-in-JS calculation. No framework initialization.
Subsequent navigation: Small CSS/JS files are cached. No framework re-initialization on page load.
Memory usage: No framework objects in memory. Lower RAM consumption, especially on mobile devices. Any exposed javascript state is driven by application needs, not framework mandates.
Users on slow connections (3G, rural broadband, data caps) see dramatic improvement. The login page loads in ~0.5 seconds on 3G instead of ~3 seconds.
For a login page this perception of speed matters. It’s the first thing and maybe the last thing any potential user sees.
Accessibility
Vanilla HTML5 is more accessible than framework abstractions. I’m not an expert in this area and I can only advise you to consult a professional if accessibility really counts. I just try my best.
Native <button> elements: – Keyboard navigable by
default – Screen reader accessible – Focus management built in –
Semantic meaning preserved
Native <details> elements: – ARIA attributes automatic – Keyboard navigation (Enter/Space to toggle) – Screen reader announces state (“expanded” / “collapsed”)
Framework widgets often require custom ARIA attributes, keyboard event handlers, and focus management. Get it wrong and you’ve created accessibility barriers.
Native HTML elements are accessible by default. Somebody already invested much work to get them right. You have to work to break it.
What We Gave Up
Vanilla web development isn’t all benefits. There are tradeoffs:
No component library: We write every button, every form field, every modal from scratch if it isn’t close to the browser’s default behaviour. React has thousands of components available on npm. We have what we build. But more often than not close to browser native behaviour is just right in 2026.
No state management: No Redux, no MobX, no framework-level reactivity. State management is up to the application developer. Rely on pre-rendered HTML snippets from the server? Roll your own one way / fluent state system? Include a third party library? Fall back to manual—update DOM when data changes? That’s all up to you and you don’t even have to keep the same system from task manager to calendar to wiki. Less Bondage & Discipline Software Maintenance, more freedom to try out and change your mind as you go along.
Less built-in abstraction: Repetitive code can happen. Multiple buttons with similar styles? Copy the class names or find your own abstractions and throw them over board when you find out you want to do that. All stops out, full control means full accountability.
Smaller ecosystem: Framework communities provide plugins, extensions, tutorials. Vanilla web development is DIY. But nobody stops you from adopting individual and isolated components.
Steeper learning curve for some: Developers trained on React/Vue may need to wrap their heads around this. “How do I update the DOM?” – “Where’s my component lifecycle hooks?”
These tradeoffs are real. But for Horde’s use case is server-rendered PHP application with growing but limited frontend activity. Much of the growth in Javascript code base does not really contribute to interactivity on the front end. It’s just ecosystem cohesion and reusing to tool you already have in your belt.
We don’t need complex state management for many of our apps. Data comes from PHP. We don’t need thousands of components. We barely used those we had from the JQuery Mobile library. We don’t need our third party components to be all that much related.
The benefits (no framework deprecation cycle, lower maintenance burden, smaller bundle size) outweigh the costs.
Adoption Strategy
We didn’t convert everything at once. It’s going to be a long journey.
- Proof of concept: Login page (small, contained,
high-visibility) - First real migration: Mobile portal (moderate
complexity, mobile-only) - Convert all apps one by one: Tasks, Calendar, Mail
(more complexity, mobile-only) - Iterate: Dark theme, red theme (validate theme
system) - Public Access: Unauthenticated Guest pages
(public-facing wiki, bug tracker, project site) - Scale to larger displays: Make Responsive mode
viable for tablets and larger screens until it rivals and eclipses
desktop mode.
Well, that was the plan. By now I decided to use the tiny horde/passwd app as a step 2a before step 3. It’s super simple, it never had a mobile friendly view and it is a good test bad for shared infrastructure steps 1 and 2 didn’t need but step 3 will depend on.
Each step validates the approach before expanding. Each step delivers value independently. If we discover problems, we pivot without major disruption.
This is risk management for architectural changes.
Lessons and analogies in the backend
In the PHP backend I see a similar trend. Holistic frameworks of the 2000s have evolved into micro frameworks and component libraries. As the language itself grew and package management moved from PEAR to Composer, initiatives such as the PHP-FIG created widely adopted standards. It is now possible to mix & match DI containers, cache layers, HTTP middlewares and autoloaders across vendors and projects. Your favourite logger can be integrated into Horde just fine – at least into those parts already converted.
Looking Forward
Vanilla web development in Horde is shipping. Login and mobile portal are live and using the techniques described here.
Getting the last bits of jquery mobile out of the system will be an important moment. It will validate our approach. Until then we are essentially just adding complexity.
But the foundation is solid and the outlook is good. Code we write today will work in 2030 without migration. That long-term stability is worth the upfront investment.
For volunteer open source projects, sustainable architecture beats fashionable technology every time.
Coming up in Part 4: Developer infrastructure improvements—real database testing, PHPUnit modernization, and lowering barriers to contribution.
This is Part 3 of a 4-part series on Horde 6’s architectural evolution. Part 1 covered jQuery Mobile deprecation, Part 2 covered JWT authentication, and Part 4 covers developer infrastructure improvements.
Leave a Reply