next-gen web UI framework for Go

write Go to change HTML on the go

as explicit as it gets

directly update HTML as UI events occurs

← Click

// composable ui fragment
type Counter struct {
    // dynamic element
	node  doors.Node
    // some state
	count int
}

// click handler function
func (c *Counter) handler(ctx context.Context, _ d.REvent[d.PointerEvent]) bool {
	c.count += 1
	// updading node with new new content
	c.node.Update(ctx, c.display())
    // not done, keep hook active
	return false
}

// display click count
templ (c *Counter) display() {
	Clicked { fmt.Sprint(c.count) } time(s)!
}

// fragment render function
templ (c *Counter) Render() {
    // render dynamic element
    @c.node {
        // initial content
        ← Click
    }
    // button with attribute constructor and attached handler on click action
	<button { d.A(ctx, d.AClick { On: c.handler })...}>
        Click Me!
    </button>
}

superior routing

reactive, typed and declarative

id: 9, filter:
// *this* page path is deserialized into struct (model)
type Path struct {
    // path option 1: root
	Main  bool `path:""`
    // path option 2: /show + parameter value, save to Id 
	Show   bool `path:"show/:Id"`
	Id     int
    // query parameter
	Filter string `query:"filter"`
}

// prepare simple components to show on different path variants
templ home() {
	Just home page
}
templ show(id int, filter string) {
	id: { fmt.Sprint(id) }, filter: { filter }
}

// functional component
// Beam - source of path model updates (and also state piece, that you can create yourself)
templ Demo(beam doors.Beam[Path]) {
    // subscribe to path changes and render based on active path
	@doors.Sub(beam, func(p Path) templ.Component {
        // if option 1 is active - render home
		if p.Main {
			return home()
        // if option 2 is active - render show
		} else if p.Show {
            return show(p.Id, p.Filter)
		}
        // render nothing otherwise
		return nil
	})
    // prepare model to form href attr
	{{ model := common.MainPath { Main: true } }}
	// construct link with model and active link setup
	<a { d.A(ctx, d.AHref { Model: model })... }>
		Main 
	</a>
	// prepare model for another path variant
	{{ model = common.MainPath { Show: true, Id: 9, Filter: "color" } }}
	<a { d.A(ctx, d.AHref { Model: model } )... }>
		Show #9
	</a>
}

it's time to switch branches

In programming, a bad decision is like node_modules directory — it never stops growing.

Sometimes you have to step back to realize you’ve been solving problems that shouldn’t have existed in the first place.

I couldn’t shake that feeling — whether I was using frontend frameworks or dealing with sophisticated API access control schemas on the server side.

Now, after years of building and learning, I finally have the experience — and resources — to offer an alternative.

— founder

native and friendly form handeling

use deserialization or deal with HTTP request yourself

Submit Form
 // form data for parsing
type FormData struct {
	Name  string `form:"name"`
	Email string `form:"email"`
}

// composable ui fragment
type FormFragment struct {
	// dunamic mode
	n doors.Node
}

// form handler
func (f *FormFragment) handler(ctx context.Context, r doors.RForm[FormData]) bool {
	// wait some time for demo purposes
	<-time.NewTimer(time.Second).C
	// show submitted data
	f.n.Update(ctx, f.display(r.Data()))
	return false
}

// display form values
templ (f *FormFragment) display(d FormData) {
	name: { d.Name }, email: { d.Email }
}

// let's setup some form pending indication, that we will use later
var indicators = []doors.Indicate{
	// set attribute to #form-submit
	doors.IndicateAttrQuery("#form-submit", "aria-busy", "true"),
	// change content of #form-submit
	doors.IndicateContentQuery("#form-submit", "Simulating delay..."),
}

templ (f *FormFragment) Render() {
	@f.n {
		// initial dynamic node content
		Submit Form
	}
	<form
	    // construct form attributes to hook on form submittion 
	    // you can also use doors.ARawSubmit if you want to do form parsing/reading from http request yourself
		{ doors.A(ctx, doors.ASubmit[FormData]{ 
	        // use our indication
            Indicate: indicators, 
	        // block form submission until previous one is processed
            Mode: doors.ModeBlock(), 
	        // attach handler
            On: f.handler },
        ) ... }
	>
		<input
			name="name"
			placeholder="Name"
			required="true"
		/>
		<input
			type="email"
			name="email"
			placeholder="Email"
			required="true"
		/>
		<button	id="form-submit" role="submit">Submit</button>
	</form>
}

server-driven for the real world

technology you shouldn’t avoid anymore

⚡ Responsive
doors uses seamless rolling requests with stream body reading and a robust sync protocol.
Unlike common WS or SSE approaches, which are prone to slow disconnect detection, wakeup sloppiness, half-open states, and silent drops.
⛓️‍💥 Unbounded
Advanced concurrency model enables parallel event processing, state propagation, and rendering — without race conditions.
No compromises in favor of internal simplicity.
👫 Straight
Hooks as regular HTTP request handlers, composable front-end style state and components, native routing, seamless JS integration.
Clean combination of SPA and MPA techologies with minimal artifacts.

seamless frontend ↔ backend integration

trigger Go hooks from JS, call JS functions from Go, and pass data effortlessly

/*
    in this *dirty* demo we will pass data to frontend, 
    then use that data in JS to trigger Go hook 
    in which js code will be called to show alert... 
*/
templ Demo() {
    // generate id
	{{ id := uuid.New().String() }}
    // use generated id 
	<button id={ id }></button>
    // first we save attributes to a variable for readablity
	{{ attrs := doors.A(ctx,
        // pass data to frontend
        doors.AData{
            Value: id,
            Name:  "id",
        },
        // attach hook with int input and int output
        doors.AHook[int, int]{
           // block until previous one is processed
            Mode: doors.ModeBlock(),
            // hook name
            Name: "callback",
            // hook handler
            On: func(ctx context.Context, r doors.RHook[int]) (int, bool) {
                // make "alert" call to the frontend (check call handler in script below)
                doors.Call(ctx, doors.CallConf{
                    Name: "alert",
                    Arg:  fmt.Sprint("Go will multiply: ", r.Data()),
                    /* you can also process js call return value and respond to it */
                })
                // hook response
                return r.Data() * 2, false
            },
        },
    ) }}
    // 
    // convert inline script to script with src  
    // access atributes with $d, don't worry about global scope, and use async
    @doors.Script {
        // use our saved attributes on a js webpage script
        <script { attrs... } />
            // read data "id" from using special $d object (scoped to current script for attrubute reading)
            const id = $d.data("id")
            // use id to get button element
            const button = document.getElementById(id);
            let total = 1;
            button.innerHTML = `multiply ${total} by 2 in Go`
            // attach call with name "alert" (to closest dynamic parent element or root), 
            $d.on("alert", (message) => {
                // just alert whatever go gives
                alert(message)
                /* you can also provide return value here */
            })  /* <- you can also handle Go response to call result (🤯) in third argument*/
            // add plain event listener to our button
            button.addEventListener('click', async () => {
                try {
                   // trigger hook  with the name "callback" and total value as argument
                   total = await $d.hook("callback", total)
                   // update button content with the response
                   button.innerHTML = `multiply ${total} by 2 in Go`
                }catch(e) {
                    // check if hook blocked (we set block mode, so it's ok)
                    if(e.isBlocked()) {
                        console.log("Call blocked")
                        return
                    }
                    throw e
                }
            })
        </script>
    }
}

complete web dev toolkit

and different in a good way

🧬 transperent & lean state managment
  • create state sources and derive new ones from it
  • update or mutate state, all changes will be distributed in a parallel and safe way
  • subscribe to introduce controlled DOM updates
⚡ performant & controlled
  • parallel rendering & event handeling
  • controlled resource consumption with a goroutine pool and instance limit per session
  • extra-thin client side
✨ UX-friendly
  • indication for pending operations and active links
  • debouncing and other event handeling modes
  • integration of js apps and modules for UI-heavy stuff
🧩 server driven superpowers
  • no exposed endpoints you don't have to deal endpoinds at all
  • always SSR hydration hell is in the past
  • unique page instance users can only interact with what they see
  • minimal js zero NPM dependencies
📦 builds & serves
  • build your js/ts modules
  • cook import map & CSP headers
  • host files in protected or public modes
  • transform inline scripts and styles to external
🚀 set up & run with no hassle
  • it's just router - use it with standart Go http server
  • build with awesome templating solution - templ
  • batteries included bundle and build js/ts without extra tools

a lifetime affordable license

to ensure independence and sustainability

  • not backed by big company or VC funding
  • not relying on spare-time maintanance
  • here to give you the best tool

don't miss

tell me that you are interested, get email on the beta launch - no subscription