Karax: a small virtual DOM framework for Nim
Karax. Single page applications for Nim.
At a glance
- What is it?
- Karax is a framework for building single page applications in Nim, with a virtual DOM, a macro based buildHtml DSL that generates the tree construction code for you, and no external dependencies, so the generated JavaScript contains only the code you actually use. It suits a Nim developer who wants a reactive web front end without pulling a JavaScript toolchain into the build, and its reactivity model is event driven rather than state driven.
- Who is it for?
- Adopt Karax if you write in Nim and want a single language across your whole application, because the whole program optimization of Nim means the output JavaScript carries only the code you use, with no runtime library to load. Do not adopt it if your front end needs a large component ecosystem, because the framework is a virtual DOM and a DSL with a handful of examples rather than a platform.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 103 days ago.
- What is it written in?
- Mainly Nim, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What Karax is, and the claim it makes
Karax is a framework for developing single page applications in Nim, and the README sets out its proposition in a few sentences.
It uses a virtual DOM like React, but is much smaller than the existing frameworks, and, of course, it is written in Nim for Nim. There are no external dependencies, and thanks to Nim's whole program optimization only what is used ends up in the generated JavaScript code.
That last point is the one that distinguishes it from every other approach to this problem. The usual way to build a front end for a compiled language is to write JavaScript and glue it to the backend with an API layer, which means two languages, two toolchains and a serialization boundary in the middle. Karax compiles Nim to JavaScript through the js target, so the application is one language and one build, and the output is a single JavaScript file with no runtime bundled in, because the compiler proves what is unreachable and drops it.
The goals are stated as two lines. Leverage Nim's macro system to produce a framework that allows for the development of applications that are boilerplate free, and keep it small, keep it fast, keep it flexible.
The licence is MIT, the current version is 1.5.0 from April 2025, with 1.4.0 from the same day and 1.3.3 from November 2023, and the last push to the repository was on 2026-07-12.
Installing it and running the examples
To use Karax you must have Nim installed, and then it installs through nimble.
nimble install karaxThe try-it path is slightly different, because it clones the repository and links it into the nimble directory instead of installing a released version.
nimble develop karax
cd karax
cd examples/todoapp
nim js todoapp.nim
open todoapp.htmlThat is the whole loop. A Nim file is compiled to JavaScript with the js target, and the resulting HTML file is opened in a browser. The same pattern is repeated for the media player example, which compiles a second program and opens its html file.
The examples directory is a good map of what the framework can do. There is a hello world, a hello universe, a button, a button with a lambda, a styled hello, a login form, a slider, a scroll application, a carousel, a todo app, a media player, a toy chat, a karaxtmulti example, an ajax example, and a config.nims file that the examples use to share build settings.
The presence of a media player and a carousel is worth noting, because those are the cases where a virtual DOM earns its keep. A todo list works with any framework, but a carousel with animation and a media player with a timeline are where a diffing approach either works well or does not.
The buildHtml DSL and the code the macro produces
The simplest Karax program is short, and the DSL is the reason.
include karax / prelude
proc createDom(): VNode =
result = buildHtml(tdiv):
text "Hello World!"
setRenderer createDomSince div is a keyword in Nim, Karax uses tdiv instead, and tdiv produces a div virtual DOM node. The DSL is a macro, and the README shows what it generates with the debug flag, and the generated code is where the design becomes clear.
let tmp1 = tree(VNodeKind.tdiv)
add(tmp1, text "Hello World!")
tmp1The DSL introduces temporaries and calls add for the tree construction, and the reason given is that it composes with all of Nim's control flow constructs. The example that proves it takes a random branch inside the DSL, with an if and an else around the text, and the generated code keeps the if and the else as ordinary Nim control flow rather than building a data structure that is then interpreted.
That is the point of the whole approach. In a framework where the tree is described as data, a conditional in the markup is a conditional in the data, and the author argues that languages without an AST based macro system cannot do this, which is an argument about the expressiveness of a DSL implemented as a macro rather than as a parser.
The event handler syntax shows the same idea, since a handler can be written as a named proc taking the event and the node, or as a closure.
button(onclick = () => lines.add "Hello simulated universe"):
text "Say hello!"The generated code for that is an addEventHandler call with the event kind and the closure, and the README's comment on it is that as the examples grow larger it becomes more visible what a DSL that composes with the builtin control flow constructs buys you.
karun, the HTML boilerplate, and the debug flag
The DSL produces the virtual DOM tree, but a browser still needs an HTML file that loads the generated JavaScript and gives it a place to render. That is the job of karun, which the README calls a tiny build tool that generates the HTML boilerplate code that embeds and invokes the generated JavaScript code.
nim c karax/tools/karun
karax/tools/karun -r helloworld.nimThe -r flag is what ties the two together, since it runs the compile step as part of the invocation rather than assuming the JavaScript is already there.
For a framework this small, that is a notable design choice. There is no bundler, no node_modules, no transpiler and no dev server, and the pipeline is a Nim compiler, a small Nim tool, and a browser. The example loop of compile and open is the whole developer experience.
The debug flag is the other tool worth knowing. With the debugKaraxDsl define set, you can see the Nim code that buildHtml produced, which the README uses to explain what the macro is doing.
The output shown for the debug run is the same code as the non-debug output but with the identifiers shortened for readability, which is a small touch. It matters for a macro heavy framework, because a reader who cannot see what the macro emits has to trust it, and a reader who can see it can check that the generated code is what they expected.
The event model, and how handlers get their data
Karax does not change the DOM's event model much, and the example that demonstrates it writes a line of text when a button is clicked.
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button:
text "Say hello!"
proc onclick(ev: Event; n: VNode) =
lines.add "Hello simulated universe"
for x in lines:
tdiv:
text x
setRenderer createDomThe handler type is either (ev: Event; n: VNode) or (), which is where the second example comes in. Since the type of a handler is fixed, any additional data that should be passed to the handler has to come through Nim's closures, and the README shows the pattern for that, a factory proc that takes a menu entry and returns a closure.
proc menuAction(menuEntry: kstring): proc() =
result = proc() =
echo "clicked ", menuEntryThe kstring type is the other detail in that example. It is Karax's alias for cstring, which stands for compatible string, and for the JavaScript target that is an immutable JavaScript string. It is preferred for efficiency on the JS target, and on the native targets it is mapped to string for efficiency. The abstraction exists to reconcile those two conflicting requirements, and the HTML construction DSL is also available on the native targets, which is what lets the same code be compiled and tested outside a browser.
Reactivity that reacts to events
The reactivity model is the part of Karax that differs most from mainstream frameworks, and the README is direct about it.
Mainstream frameworks usually implement reactivity by creating reactive state. Karax instead reacts to events. The approach is described as simpler and easier to reason about, with the tradeoff that events need to be wrapped to trigger a redraw.
Karax does that wrapping for the cases it knows about, which are the DOM event handlers such as onclick and keyup, and the ajax network calls when you use the karax/kajax module. For anything outside of that, you have to add it yourself, and the examples given are websocket messages and document timing functions.
The timing function case is shown with a concrete workaround, because it is the one people hit first. The karax/kdom module includes a definition for setInterval, the browser API that repeatedly calls a given function, and by default it is not reactive.
proc setInterval(cb: proc(), interval: int): Interval {.discardable.} =
kdom.setInterval(proc =
cb()
if not kxi.surpressRedraws: redraw(kxi)
, interval)So the wrapped version calls the function and then triggers a redraw, unless redraws are suppressed.
This is the trade-off in one line. A reactive state system updates the screen when state changes, so a timer that changes a variable redraws by itself. An event driven model redraws when something happens, so a timer that changes a variable is an event you have to describe. The first is more automatic and the second is more explicit, and the second is also cheaper, because nothing redraws unless you said so.
The limits and the state of the project
Three things to weigh.
The first is that this is a framework for a niche language. Karax is written in Nim, and while the DSL and the virtual DOM ideas are portable, the implementation is not. A team that is not already using Nim is being asked to adopt a language as well as a framework, and Nim's own ecosystem is where the real constraint lies.
The second is the size of the ecosystem. The examples directory is the honest inventory: a todo app, a carousel, a media player, a chat, a login form, an ajax example. There is no component library, no router, no state management package and no testing helpers to speak of. That is what a small framework looks like, and the README is honest about the ambition, keep it small, keep it fast, keep it flexible.
The third is the project state. The version is 1.5.0, released in April 2025 alongside 1.4.0, with the previous release 1.3.3 from November 2023, and the last push was on 2026-07-12. That is an active repository with a slow release line, and the release dates should tell you what to expect from the package manager.
What the project has going for it is the argument at the centre of its design. A compiled language that emits JavaScript with no runtime and no unused code, with a DSL that composes with the language's own control flow, is a coherent position, and the small scope is deliberate rather than a sign of an unfinished project.
Editorial conclusion
Adopt Karax if you write in Nim and want a single language across your whole application, because the whole program optimization of Nim means the output JavaScript carries only the code you use, with no runtime library to load. Do not adopt it if your front end needs a large component ecosystem, because the framework is a virtual DOM and a DSL with a handful of examples rather than a platform. Before you commit, read the reactivity section, because the model reacts to events instead of tracking state, and any callback that is not a DOM handler or an ajax call has to trigger a redraw by hand.
Frequently asked questions
What is Karax?
It is a framework for developing single page applications in Nim. It uses a virtual DOM like React but is much smaller, has no external dependencies, and thanks to Nim's whole program optimization only the code you actually use ends up in the generated JavaScript.
How do I install and try Karax?
Install it with nimble install karax. To try the source, run nimble develop karax, then compile one of the examples with the js target, for example nim js todoapp.nim from the examples/todoapp directory, and open the generated todoapp.html in a browser.
How does Karax handle reactivity?
Unlike mainstream frameworks, Karax reacts to events rather than tracking reactive state. DOM event handlers and ajax calls trigger a redraw automatically, but other callbacks, such as websocket messages or setInterval timers, have to be wrapped to call redraw themselves.
What does buildHtml do in Karax?
It is a macro based DSL for constructing virtual DOM trees of type VNode. It expands to ordinary Nim code that creates temporaries and calls add, which is what lets it compose with Nim's own control flow constructs, and the expanded code can be inspected with the debugKaraxDsl define.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/karaxnim-karax)Was this useful?