# JavaScriptKit: calling JavaScript from Swift compiled to WebAssembly

> JavaScriptKit is the Swift framework that gives code compiled to WebAssembly a way to reach JavaScript objects, functions and promises, with a BridgeJS plugin for typed bindings in both directions. It requires Swift 6.3 or newer and ships as an MIT licensed SwiftPM package with a JavaScript runtime.

**swiftwasm/JavaScriptKit** — Swift framework to interact with JavaScript through WebAssembly.

- Repository: https://github.com/swiftwasm/JavaScriptKit
- Website: https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit
- Stars: 991 · Forks: 77
- Language: Swift
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/swiftwasm-javascriptkit

## The boundary problem for Swift on WebAssembly

Swift compiles to WebAssembly, and WebAssembly has no DOM. The moment your Swift code runs in a browser it needs to reach JavaScript to touch the document, to register an event handler, to call a library that only exists in JavaScript, and to wait on a promise. JavaScriptKit is the framework that provides that bridge, and the README describes it as a Swift framework to interact with JavaScript through WebAssembly.

The feature list is short and specific: access JavaScript objects and functions, create closures that can be called from JavaScript, convert between Swift and JavaScript data types, use JavaScript promises with Swift's async and await, and work with multi-threading.

That last item is the one to notice. Multi-threading in a browser means Web Workers, and the Examples directory contains an ActorOnWebWorker example alongside Multithreading, which tells you the framework is built with the Swift concurrency model rather than bolted onto it. The rest of the example set, Basic, Embedded, ExportSwift, ImportTS, MultiModule, NodeJS, OffscrenCanvas, PlayBridgeJS and Testing, maps almost exactly onto the feature list, which is the best documentation the repository offers.

The audience is therefore specific. If you are writing a Swift package that has to work in a browser, or you want to move an existing Swift codebase to the web, this is the layer that makes the move possible.

## Reaching JavaScript objects and attaching Swift closures

The entry point is JSObject.global, which exposes the global scope. From there you call JavaScript functions and set properties, and Swift values convert to their JavaScript counterparts as part of the call.

```swift
let document = JSObject.global.document

// Create and manipulate DOM elements
var div = document.createElement("div")
div.innerText = "Hello from Swift!"
_ = document.body.appendChild(div)

// Handle events with Swift closures
var button = document.createElement("button")
button.innerText = "Click me"
```

The event handler is the part with real design in it. A Swift closure is wrapped in a JSClosure and assigned to the onclick property, and the closure body uses .object(...) and returns .undefined. The result is a JavaScript function value that the browser can call, with Swift code running inside it.

Two properties of this design matter for anyone using it. First, the return value is a JavaScript value, not a Swift optional or a Result, so the .object and .undefined cases are explicit rather than implicit. Second, the closure is captured by the JavaScript side, so its lifetime is governed by JavaScript reachability rather than by Swift scope, which is the sort of thing that decides whether a Swift value stays alive.

The README links a JavaScript Interop Cheat Sheet in the documentation for the full type mapping, and a Hello World tutorial for the first steps.

## BridgeJS turns the boundary into typed declarations

Reading raw JSObject calls gets old quickly, because every property access is dynamically typed on the Swift side. BridgeJS is the plugin that addresses this, and the README labels it experimental with the note that APIs may change in future releases.

It works in both directions. Exporting Swift to JavaScript lets you expose Swift functions, classes and types, and the plugin also generates TypeScript definitions, as .d.ts files, for the exported API. Importing JavaScript into Swift makes JavaScript and TypeScript APIs callable from Swift with type-safe bindings, declared either with Swift macros or generated from a TypeScript declaration file named bridge-js.d.ts.

The playground at swiftwasm.org exists because of this design. Since the exposed interface is partly derived from a declaration file and partly from Swift annotations, you cannot always see what will be on each side by reading one source file. The BridgeJS Playground lets you preview what interfaces will be exposed on the Swift and TypeScript sides before you commit to the binding.

The repository layout backs the split up. Plugins holds the BridgeJS plugin with its own README for architecture details and a PackageToJS plugin directory whose Templates directory holds runtime.mjs and runtime.d.ts, and the Makefile target regenerate_swiftpm_resources copies the built runtime files into those templates. The plugin has its own test suite with TypeScript snapshot tests, checked by the check:bridgejs-dts script running tsc against a snapshot project.

## Exporting Swift classes with the @JS macro

On the Swift side, the @JS macro marks what should be visible to JavaScript. Applied to a class, its properties, its initialiser and its methods, it describes a Swift type that JavaScript can construct and call:

```swift
@JS class Greeter {
    @JS var name: String

    @JS init(name: String) {
        self.name = name
    }

    @JS func greet() -> String {
        return "Hello, \(name)!"
    }
}
```

On the JavaScript side, the exported type appears under exports, and the class is constructed with new and its methods called like any other object method:

```javascript
const greeter = new exports.Greeter("World");
console.log(greeter.greet()); // "Hello, World!"
```

Two things are worth understanding here. The Swift initialiser becomes a JavaScript constructor, so a Swift type with a throwing initialiser or a complex argument list may not map cleanly, and the @JS macro is doing the translation work rather than exposing the raw Swift ABI. Second, the TypeScript definitions the plugin generates are the contract that JavaScript callers actually see, so the exported surface is described twice, once in Swift and once in .d.ts, and the plugin keeps them in step.

The Examples directory has an ExportSwift directory for this direction of interop.

## Importing JavaScript with @JSClass and friends

The other direction uses a small family of macros. @JSClass declares a struct that mirrors a JavaScript class, @JSFunction marks a method that calls a JavaScript function, @JSGetter and @JSSetter expose properties, and @JS marks a top-level function as callable from JavaScript.

```swift
@JSClass struct Document {
    @JSFunction func getElementById(_ id: String) throws(JSException) -> HTMLElement
    @JSFunction func createElement(_ tagName: String) throws(JSException) -> HTMLElement
}
@JSGetter(from: .global) var document: Document

@JSClass struct HTMLElement {
    @JSGetter var innerText: String
    @JSSetter func setInnerText(_ newValue: String) throws(JSException)
    @JSFunction func appendChild(_ child: HTMLElement) throws(JSException)
}
```

Note the error handling. Every call that can fail in JavaScript is declared as throwing a JSException, so a Swift caller writes try document.createElement("button") rather than discovering a failure as a nil or a crash. That is the single biggest ergonomic difference from the raw JSObject style, where the same operations are unchecked.

The same bindings can be generated from a TypeScript file, bridge-js.d.ts, instead of being written by hand. For a team that already has TypeScript definitions for the library it wants to call, generating the Swift side from those definitions keeps the two in step, and the ImportTS example demonstrates that path. For a small surface, writing the macros by hand is shorter and gives you a place to document the contract.

## Building and testing the package and its runtime

JavaScriptKit is a SwiftPM package with a Package.swift at the root, and it also carries a JavaScript side. The package.json describes a npm package named javascript-kit-swift whose main entry is Runtime/lib/index.js, with a module build at Runtime/lib/index.mjs and types at Runtime/lib/index.d.ts. The version there is 0.0.0, which reads as a placeholder rather than the version of the Swift package.

The JavaScript side is built with rollup and TypeScript, and the runtime tests run under vitest. The Makefile bootstraps the npm dependencies first:

```bash
npm ci
```

The unit test target needs a Swift SDK for WebAssembly to be selected explicitly. The Makefile checks that SWIFT_SDK_ID is set and tells you to run swift sdk list and pass a matching SDK, then invokes the Swift package manager with the js test subcommand and a prelude file, with node options that expose the garbage collector:

```bash
swift package --build-system "$(BUILD_SYSTEM)" --swift-sdk "$(SWIFT_SDK_ID)" \
    $(TRACING_ARGS) \
    --disable-sandbox \
    js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc
```

The build system defaults to swiftbuild and the tracing trait can be disabled with JAVASCRIPTKIT_DISABLE_TRACING_TRAIT set to 1, which is useful when you want the tests to run without the instrumentation overhead.

Two consequences for a team. The test suite needs a WebAssembly Swift SDK installed, not just a Swift toolchain, so CI setup is more than installing Swift. And because the runtime is a separate npm artifact, the Swift package and the JavaScript runtime must be version-compatible, which is the practical meaning of the fast release cadence.

## Swift 6.3 minimum, MIT licence, frequent releases

The minimum supported Swift version is 6.3, stated in the README. That is a hard floor, and it matters because a project that still builds on an older Swift cannot adopt JavaScriptKit without a toolchain upgrade.

The licence is MIT, with the LICENSE file at the repository root and the same licence named in package.json. For a framework that other packages depend on, that is the licence that lets it sit inside a proprietary product without a negotiation.

On maintenance, the repository is not archived and the last push was on 2026-10-19, with version 0.61.0 released the same day, 0.60.0 on 2026-10-15 and 0.59.0 on 2026-09-07. Three releases in six weeks is a fast cadence for a framework, which cuts both ways. You get bug fixes and Swift toolchain compatibility quickly. You also get breaking changes quickly, since a zero major version with a 0.x numbering and a BridgeJS plugin whose APIs may change in future releases is a moving target.

The practical approach is to pin the version, read the CHANGELOG.md at the repository root before upgrading, and keep the Swift and JavaScript sides of your dependency at matching versions. The homepage points at the Swift Package Index documentation for the package, and the repository also carries CONTRIBUTING.md for the contribution process.

## Conclusion

JavaScriptKit is the right choice when you are shipping Swift to the browser or to Node through WebAssembly and need to touch the DOM, callbacks or JavaScript libraries without dropping to raw JavaScript. It is the wrong choice if you need a synchronous DOM call on the main thread, since every crossing is asynchronous and the framework cannot block, or if your project is on an older Swift toolchain, because the minimum supported Swift version is 6.3 and the release cadence is fast enough that staying current is part of the deal. Verify first that the version of JavaScriptKit you pin matches the Swift SDK you build with, that the BridgeJS plugin output you generate is committed or reproducible, since the plugin is labelled experimental and its APIs may change in future releases, and that the JavaScript runtime you ship comes from the npm package rather than a hand-maintained copy.

## FAQ

### What Swift version does JavaScriptKit require?

The minimum supported Swift version is 6.3. The repository is not archived and the last push was on 2026-10-19, with releases 0.59.0, 0.60.0 and 0.61.0 in the weeks before that.

### How do I call a JavaScript function from Swift with JavaScriptKit?

Reach the global scope through JSObject.global and call through the returned object, for example document.createElement("div"). The BridgeJS plugin adds typed alternatives using macros such as @JSClass, @JSFunction, @JSGetter and @JSSetter, where calls that can fail in JavaScript are declared as throwing a JSException.

### What does the BridgeJS plugin do?

It provides typed interoperability in both directions: exporting Swift functions, classes and types to JavaScript while generating TypeScript definitions, and importing JavaScript or TypeScript APIs into Swift with macros or bindings generated from a bridge-js.d.ts file. The README labels it experimental and warns that APIs may change in future releases.

### How do I run the JavaScriptKit unit tests?

The Makefile unit test target requires SWIFT_SDK_ID to be set to a WebAssembly Swift SDK, which you find with swift sdk list, and then runs the js test subcommand with a prelude file. The npm dependencies are installed first with npm ci.

### What licence is JavaScriptKit released under?

MIT, with the licence text in the LICENSE file at the repository root and the same licence declared in package.json.

## Sources

- [License: MIT](https://github.com/swiftwasm/JavaScriptKit/blob/main/LICENSE)
- [Project website](https://swiftpackageindex.com/swiftwasm/JavaScriptKit/documentation/javascriptkit)
- [README](https://github.com/swiftwasm/JavaScriptKit/blob/main/README.md)
- [Releases](https://github.com/swiftwasm/JavaScriptKit/releases)
- [swiftwasm/JavaScriptKit on GitHub](https://github.com/swiftwasm/JavaScriptKit)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/swiftwasm-javascriptkit
