Skip to content

foldkit/foldkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,426 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Foldkit

npm version

The frontend framework for correctness.

Documentation · Manifesto · Examples · Getting Started · Discord


Foldkit is a TypeScript frontend framework built on Effect and architected like Elm. One Model, one update function, one way to do things. No hooks, no local state, no hidden mutations. It's all in on Effect with no escape hatch, though a program doesn't have to own the whole page: Runtime.embed runs a Foldkit widget inside any existing app, React included.

Your Model is a Schema and side effects are values you return, not callbacks you fire. If you know Effect, Foldkit feels natural. If you're new to it, Foldkit is a good way in. Coming from React? Start here, or read the same pixel-art editor built in both frameworks.

Note

Foldkit is pre-1.0. The core API is stable, but breaking changes may occur in minor releases. See the changelog for details.

Get Started

create-foldkit-app scaffolds a complete setup with Tailwind, TypeScript, Oxlint, Prettier, and the Vite plugin for state-preserving HMR, starting from an example you choose.

npx create-foldkit-app@latest

Counter

A complete Foldkit program. State lives in a single Model, events become Messages, and a pure function handles every transition. main.ts defines the program and entry.ts boots the runtime, so main.ts stays importable from tests without booting a runtime as a side effect.

// src/main.ts
import { Match as M, Schema as S } from 'effect'
import { Command, Runtime } from 'foldkit'
import { Document, html } from 'foldkit/html'
import { m } from 'foldkit/message'

// MODEL

export const Model = S.Struct({ count: S.Number })
export type Model = typeof Model.Type

// MESSAGE

const ClickedDecrement = m('ClickedDecrement')
const ClickedIncrement = m('ClickedIncrement')
const ClickedReset = m('ClickedReset')

export const Message = S.Union([
  ClickedDecrement,
  ClickedIncrement,
  ClickedReset,
])
export type Message = typeof Message.Type

// UPDATE

export const update = (
  model: Model,
  message: Message,
): readonly [Model, ReadonlyArray<Command.Command<Message>>] =>
  M.value(message).pipe(
    M.withReturnType<
      readonly [Model, ReadonlyArray<Command.Command<Message>>]
    >(),
    M.tagsExhaustive({
      ClickedDecrement: () => [{ count: model.count - 1 }, []],
      ClickedIncrement: () => [{ count: model.count + 1 }, []],
      ClickedReset: () => [{ count: 0 }, []],
    }),
  )

// INIT

export const init: Runtime.ApplicationInit<Model, Message> = () => [
  { count: 0 },
  [],
]

// VIEW

export const view = (model: Model): Document => {
  const h = html<Message>()

  return {
    title: `Counter: ${model.count}`,
    body: h.div(
      [],
      [
        h.p([], [model.count.toString()]),
        h.button([h.OnClick(ClickedDecrement())], ['-']),
        h.button([h.OnClick(ClickedReset())], ['Reset']),
        h.button([h.OnClick(ClickedIncrement())], ['+']),
      ],
    ),
  }
}
// src/entry.ts
import { Runtime } from 'foldkit'

import { Model, init, update, view } from './main'

const application = Runtime.makeApplication({
  Model,
  init,
  update,
  view,
  container: document.getElementById('root'),
})

Runtime.run(application)

Source: examples/counter.

What Ships With Foldkit

A complete system, not a collection of libraries you stitch together. Each of these is documented in depth at foldkit.dev.

  • Commands: Side effects as named Effects that return Messages and are run by the runtime.
  • Routing: Type-safe bidirectional routing from parser combinators. URLs parse to Routes, Routes build URLs.
  • Subscriptions: External event streams declared as a function of the Model.
  • Managed Resources: Model-driven lifecycle for WebSockets, AudioContext, and other long-lived handles.
  • Mount: The seam where view code hands a real DOM element to a third-party library that owns its own DOM.
  • Submodels: A self-contained Model, update, and view that a parent embeds, wrapping child Messages in a Got* envelope.
  • OutMessage: A typed channel for a child Submodel to emit domain events up to its parent.
  • Embedding: Run a Foldkit program inside a host app through Schema-typed Ports with Runtime.embed.
  • UI Components: Accessible, keyboard-friendly primitives in the @foldkit/ui package.
  • Field Validation: Per-field validation state modeled as a discriminated union.
  • Virtual DOM: Declarative views with lazy memoization and keyed diffing, powered by Snabbdom.
  • DevTools: In-browser overlay for inspecting Messages, Model, and Commands, with time-travel.
  • DevTools MCP: Expose a running app to AI agents over the Model Context Protocol.
  • Crash View and Reporting: A custom fallback UI when the update loop throws, plus a report callback.
  • Story Testing: Exercise the update function directly, resolving Commands inline. No mocks, no fake timers.
  • Scene Testing: Drive your real view the way a user does, with accessible locators. No browser required.
  • Slow Warnings: Development warnings when update, view, patch, or Subscription extraction exceeds its budget.
  • HMR: Vite plugin with state-preserving hot module replacement. Change your view, keep your state.

Correctness You (And Your LLM) Can See

Every state change flows through one update function, and every side effect is declared explicitly. You don't have to hold a mental model of what runs when, you can point at it. That's what makes Foldkit unusually AI-friendly: the property that makes the code easy for humans to reason about makes it easy for an LLM to generate and review.

Examples

Some of what you can build with Foldkit. See all example apps on foldkit.dev.

  • Counter: Increment/decrement with reset
  • Todo: CRUD operations with localStorage persistence
  • Form: Form validation with async email checking
  • Job Application: Multi-step form with cross-field validation, file uploads, and per-step error indicators
  • Weather: HTTP requests with async state handling
  • API Cache: Query caching with stale-while-revalidate, request deduplication, and interval refetching
  • Routing: URL routing with parser combinators
  • Route Transitions: Live transition log with entry, exit, and stayed navigation policies
  • Query Sync: URL query parameter sync with filtering and sorting
  • Snake: Classic game built with Subscriptions
  • Auth: Authentication flow with Submodels and OutMessage
  • Shopping Cart: Nested models and complex state
  • WebSocket Chat: Managed Resources with WebSocket integration
  • Kanban: Drag-and-drop kanban board with cross-column reordering and keyboard navigation
  • Pixel Art: Grid-based pixel editor with painting, erasing, and palette selection
  • UI Showcase: Interactive showcase of every Foldkit UI component
  • Typing Game: Multiplayer typing game with Effect RPC backend (play it live)

Development

git clone https://github.com/foldkit/foldkit.git
cd foldkit
pnpm install

# Build the Foldkit libraries in watch mode
pnpm dev:libs

# Run an example (in a separate terminal)
pnpm dev:example:counter

External reference repositories under repos/ are vendored in as git subtrees, so they come down with the clone. Each is pinned to the release tag matching the version this repo depends on, not a moving branch, so the reference source always matches what installs and compiles. Refresh repos/effect from the effect@<version> tag that matches package.json, and re-pin it whenever the effect dependency is bumped:

VER=$(node -p "require('./packages/foldkit/package.json').devDependencies.effect")
git subtree pull --prefix=repos/effect https://github.com/Effect-TS/effect.git "effect@${VER}" --squash

License

MIT

Releases

Sponsor this project

Packages

Used by

Contributors

Languages