A few years ago, while working on a Java and Thymeleaf application, I faced an interesting frontend challenge.
One of its workflows involved collecting notary-related information through a form that spanned multiple pages, with different validation requirements along the way.
The challenge wasn't creating inputs or rendering HTML. It was coordinating the entire experience while preserving user progress between separate documents.
That problem eventually inspired Demetria, a JavaScript library I recently revisited and rebuilt as an open-source project.
In this article, I want to share the problem that inspired it, the architecture behind the current implementation, and some of the trade-offs involved in building a framework-agnostic library.
01. The problem with multi-page forms
When working with modern frontend frameworks, it's easy to take client-side state management for granted.
But in a traditional server-rendered application, navigating between pages can mean loading an entirely new HTML document.
Any state stored exclusively in JavaScript memory disappears.
For a complex form, this creates several challenges:
- Preserving previously entered values.
- Tracking progress through multiple steps.
- Enforcing validation before moving forward.
- Keeping form behavior consistent between pages.
- Avoiding duplication of navigation and state-management logic.
In the original application, I used localStorage to maintain form state between pages.
That solved the immediate problem, but it also raised a broader architectural question:
Could we describe an entire multi-step form as data and let a reusable library handle its rendering, state, validation, and navigation?
That was the idea behind Demetria.
The name itself came from Demeter, the Greek goddess associated with agriculture and seasonal cycles. I liked the analogy between the progression of seasons and the different stages of a form.
02. Rebuilding an old idea
Years later, I returned to Demetria with more experience in frontend architecture and library design.
I didn't want to build another comprehensive form management framework.
Libraries such as React Hook Form already solve many problems extremely well, but my goal was different.
I wanted something that could work in a modern application or a traditional server-rendered system without requiring either one to adopt a particular frontend framework.
The initial requirements were:
- Use plain JavaScript or TypeScript.
- Accept JSON-compatible form definitions.
- Generate native HTML elements.
- Support multiple steps and built-in validation.
- Allow optional persistence across HTML documents.
- Leave visual styling to the consuming application.
- Keep runtime dependencies at zero.
- Treat accessibility as a core design concern.
These requirements shaped the boundaries of the library.
Rather than building a large ecosystem, I wanted a small tool with a clearly defined responsibility.
03. Architecture: separating responsibilities
One of the most important decisions was keeping form orchestration separate from presentation.
The current architecture can be understood through three main responsibilities: the form engine, the DOM renderer, and optional persistence.
Architecture diagram
flowchart TB
A["Host application"]
B["JSON-compatible schema"]
C["createForm(options)"]
D["Form engine"]
E["Native DOM renderer"]
F["HTML form"]
G["User interaction"]
H["Optional Web Storage"]
I["Host CSS and integration"]
A --> C
B --> C
C --> D
D --> E
E --> F
F --> G
G --> D
D <--> H
A --> I
I --> F
Figure 1. A conceptual overview of Demetria's architecture and integration boundaries.
The form engine
The engine coordinates form state, validation, and navigation.
It understands the schema, current step, values, and transition rules without depending on the application's visual presentation.
This separation allows the same form behavior to be used in different integration environments.
The DOM renderer
The renderer transforms the schema into native HTML elements, including form, fieldset, legend, label, input, select, textarea, and button.
It also connects user interactions to the form engine.
Using native browser elements reduces the need for custom behavior and makes it easier to preserve familiar keyboard interactions and semantics.
Demetria deliberately ships without mandatory CSS.
Applications can use the generated styling hooks to apply their own visual identity.
Optional persistence
The third responsibility is state persistence.
By default, Demetria keeps its state in memory.
When an application needs to preserve data between page loads, it can explicitly enable sessionStorage or localStorage.
This is particularly important for the problem that originally inspired the project.
The library doesn't need to become a router or manage the server-side application lifecycle. It only needs to restore compatible form state when the application initializes it again.
That boundary keeps the implementation focused.
04. A declarative API
The public API revolves around createForm().
Instead of manually creating fields and coordinating individual inputs, the consuming application provides a JSON-compatible schema describing the form.
Here is a working example based on Demetria's v0.1.1 API.
First, create a target element:
<div id="demetria-form"></div>
<script type="module" src="./app.js"></script>
Then define the form in JavaScript:
import { createForm } from 'demetria';
const schema = {
id: 'registration',
steps: [
{
id: 'personal',
title: 'Personal information',
fields: [
{
name: 'fullName',
type: 'text',
label: 'Full name',
required: true
},
{
name: 'email',
type: 'email',
label: 'Email'
}
]
},
{
id: 'preferences',
title: 'Preferences',
fields: [
{
name: 'newsletter',
type: 'checkbox',
label: 'Receive updates'
},
{
name: 'contactMethod',
type: 'select',
label: 'Preferred contact method',
required: true,
options: [
{ label: 'Email', value: 'email' },
{ label: 'Phone', value: 'phone' }
]
}
]
}
]
};
const form = createForm({
schema,
target: '#demetria-form',
onComplete(values) {
console.log('Completed form:', values);
}
});
form.mount();
Demetria generates the form, handles transitions between steps, validates fields, and passes the completed values to the application.
Notice that email doesn't include required: true.
In Demetria, fields are optional by default. An optional email can be left empty, but a non-empty value must satisfy the applicable email validation.
This distinction is important because optional does not mean unvalidated.
Why a JSON-compatible schema?
Choosing a declarative schema creates a clear contract between configuration and behavior.
It allows a form definition to be serialized, shared, and understood without executing arbitrary JavaScript from the schema.
But that choice has trade-offs.
A declarative format can be less expressive than an API built around callbacks and custom components.
For the initial release, I preferred a smaller, predictable contract over supporting every possible form requirement.
The objective was to make the common behavior reliable before introducing greater extensibility.
05. One library, two navigation models
A particularly important architectural requirement was supporting both single-page and multi-page flows.
Single-page forms
In a single-page flow, all steps exist within the same document.
Demetria updates the active step without requiring a full browser navigation.
The engine preserves values as users move forward and backward.
Multi-page forms
In a multi-page flow, the browser loads separate HTML documents.
The host application controls navigation, while Demetria restores compatible form state through explicitly configured persistence.
For example:
const form = createForm({
target: '#demetria-form',
schema,
persistence: {
type: 'sessionStorage',
key: 'registration-flow'
},
stepId: 'preferences'
});
form.mount();
The stepId option selects the step that the current document should render.
Different pages can initialize Demetria with the same schema and persistence key while explicitly selecting their corresponding step.
This approach avoids introducing a client-side router.
It also makes the library useful in traditional applications where navigation is controlled by the server or the surrounding application.
The persistence trade-off
Browser storage is convenient, but it comes with limitations.
Both localStorage and sessionStorage are accessible to JavaScript executing in the same origin.
They should not automatically be treated as safe places for sensitive personal information.
For that reason, Demetria doesn't persist form state by default.
Applications must explicitly enable it and remain responsible for evaluating the security and privacy requirements of their data.
In applications collecting sensitive information, server-side persistence with appropriate security controls may be more suitable.
06. Accessibility is more than semantic HTML
Accessibility was one of the areas I wanted to approach differently when rebuilding Demetria.
A form-generation library has an important responsibility: the markup and behavior it produces become part of the user experience in every consuming application.
That means accessibility problems can propagate across integrations.
The renderer therefore emphasizes:
- Native semantic controls.
- Programmatically associated labels.
- Visible required-field indicators.
- Validation errors associated with their fields.
- Keyboard-operable navigation.
- Focus management after validation failures.
- Predictable focus when changing steps.
But semantic HTML alone isn't enough.
Focus and state transitions are particularly important in multi-step experiences.
A real issue discovered during testing
During the accessibility audit for v0.1.1, we found that completing a form could cause keyboard focus to be lost.
The renderer unnecessarily rebuilt the form after completion.
That removed the focused button from the document, leaving focus on the page body.
Instead of adding a workaround that manually restored focus, we removed the unnecessary renderer rebuild.
This preserved the focused element and corrected the underlying lifecycle behavior.
It was a useful reminder that accessibility issues can reveal deeper implementation problems.
The same audit identified improvements related to accessible naming, responsive reflow, contrast, and ARIA announcements.
Automated browser tests and axe-core checks helped verify the fixes, but I don't consider those results proof of complete WCAG 2.2 AA conformance.
Manual assistive-technology testing remains important.
07. Zero runtime dependencies as a design constraint
Another deliberate choice was keeping Demetria free of runtime dependencies.
The browser already provides most of the capabilities required for this problem:
- Native DOM APIs.
- HTML form controls.
- Events.
- Web Storage.
- JavaScript data structures.
Adding a large runtime dependency tree would introduce additional compatibility considerations and maintenance costs.
That doesn't make dependencies inherently undesirable.
It simply means they should solve a problem that justifies their cost.
Demetria still uses development tools for TypeScript compilation, testing, linting, and accessibility verification.
The goal is to keep the library lightweight for the applications that consume it.
08. Testing the actual distribution
One lesson I wanted to apply during the rebuild was that successfully compiling source code isn't enough to prove that a library can be consumed.
The initial historical npm release had packaging problems.
For the new implementation, I wanted to verify the actual package artifact, not only the TypeScript sources.
That meant checking the compiled JavaScript, type declarations, package exports, and tarball contents.
We also added unit tests and browser-based checks for the generated forms and documentation examples.
The first functional release, v0.1.0, established the library's core behavior.
The next patch, v0.1.1, focused on accessibility and stability improvements without introducing breaking API changes.
I prefer that sequence to rushing into new features before the existing functionality is reliable.
09. What I learned
Rebuilding Demetria reinforced a few engineering principles that have become important in my work.
A good abstraction knows what it doesn't own
Demetria handles form orchestration, but it doesn't need to become a frontend framework, router, validation ecosystem, or form-building platform.
Keeping those responsibilities separate makes the library easier to reason about.
Framework independence should solve a real problem
Being framework-agnostic isn't automatically valuable.
In this case, it supports a concrete integration requirement: generating form experiences in applications that may not use a modern frontend framework.
Accessibility is part of behavior
Labels and semantic elements matter, but accessibility also depends on navigation, focus, validation, announcements, and lifecycle behavior.
It needs to influence architecture and testing, not just markup.
Declarative APIs create valuable boundaries
A JSON-compatible schema makes the form structure explicit.
That improves portability and predictability while establishing clear limitations around extensibility.
Small, functional releases build confidence
A library doesn't need dozens of features to be useful.
A focused implementation, working examples, accurate documentation, and a reliable distribution process are a much better starting point.
10. What's next?
Demetria is still an early-stage open-source project.
Future improvements may include conditional fields, custom validation rules, and more flexible renderer customization.
But I don't want to expand the API simply to increase the number of features.
The next changes should come from actual integration needs and feedback from developers using the library.
For now, the main goal is to make the existing functionality understandable, accessible, and reliable.
Demetria started with a complex form in a server-rendered Java application.
Years later, the same idea became an opportunity to revisit library architecture, declarative APIs, browser persistence, and accessibility.
And that's probably my favorite part of the project.
Sometimes, revisiting an old engineering problem with a different perspective is enough to turn a specific solution into something reusable.
Explore Demetria
- GitHub: https://github.com/gustavoaroberto/demetria
- Documentation and live examples: https://gustavoaroberto.github.io/demetria/
- npm: https://www.npmjs.com/package/demetria
Gustavo A Roberto · Frontend Engineering, Architecture & Technical Leadership



Top comments (0)