Gleam JavaScript Target
Gleam compiles to JavaScript in addition to Erlang. This means you can write one Gleam program and run it in browsers, Node.js, Deno, or Bun — using the same language, type system, and tooling as your server-side Gleam code.
Setting the Target
Set the compile target in gleam.toml:
name = "my_frontend"
version = "1.0.0"
target = "javascript" ← compile to JS instead of Erlang
[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
lustre = ">= 4.0.0 and < 5.0.0" // optional: UI framework
Switch targets per-command without changing the config file:
gleam run --target javascript
gleam build --target javascript
gleam test --target javascript
Output Location
Build Output
──────────────────────────────────────────────────
gleam build --target javascript
Output: build/dev/javascript/
└── my_app/
└── my_app.mjs ← ES module output
└── gleam.mjs ← Gleam runtime helpers
Gleam emits standard ES modules (.mjs). Any JavaScript tool that handles ES modules — webpack, Vite, esbuild, Rollup — works with Gleam's output directly.
JavaScript FFI
Call JavaScript functions from Gleam using @external(javascript, ...):
// Call browser's console.log
@external(javascript, "console", "log")
pub fn console_log(value: a) -> Nil
// Call Math.random()
@external(javascript, "Math", "random")
pub fn math_random() -> Float
// Call a custom JS module
@external(javascript, "./helpers.mjs", "formatDate")
pub fn format_date(timestamp: Int) -> String
JavaScript FFI Syntax
──────────────────────────────────────────────────
@external(javascript, "module_or_object", "function")
pub fn gleam_name(params) -> ReturnType
"module_or_object":
"./my_file.mjs" → import from local JS file
"Math" → global JS object
"console" → global JS object
"Date" → global JS class
Writing a JavaScript Helper Module
Put JavaScript helpers in a .mjs file alongside your Gleam code:
// src/ffi.mjs
export function getCurrentUrl() {
return window.location.href;
}
export function setLocalStorage(key, value) {
localStorage.setItem(key, value);
}
export function getLocalStorage(key) {
return localStorage.getItem(key) ?? null;
}
// src/browser.gleam
@external(javascript, "./ffi.mjs", "getCurrentUrl")
pub fn current_url() -> String
@external(javascript, "./ffi.mjs", "setLocalStorage")
pub fn set_storage(key: String, value: String) -> Nil
@external(javascript, "./ffi.mjs", "getLocalStorage")
pub fn get_storage(key: String) -> String
The Lustre Framework
Lustre is Gleam's official frontend UI framework for the JavaScript target. It follows the Elm architecture — model, view, update — providing a structured way to build browser applications:
import lustre
import lustre/element.{text}
import lustre/element/html.{button, div, p}
import lustre/event
type Model = Int
type Msg { Increment; Decrement }
fn init(_) -> Model { 0 }
fn update(model: Model, msg: Msg) -> Model {
case msg {
Increment -> model + 1
Decrement -> model - 1
}
}
fn view(model: Model) {
div([], [
button([event.on_click(Decrement)], [text("-")]),
p([], [text(int.to_string(model))]),
button([event.on_click(Increment)], [text("+")])
])
}
pub fn main() {
let app = lustre.simple(init, update, view)
lustre.start(app, "#app", Nil)
}
Lustre App Flow
──────────────────────────────────────────────────
User clicks "+"
│
▼
event.on_click(Increment)
│
▼
update(model, Increment)
model + 1 → new model
│
▼
view(new_model)
renders updated UI
Dual-Target Packages
A package can target both Erlang and JavaScript by providing FFI for each:
// src/time.gleam
// Erlang implementation
@external(erlang, "erlang", "system_time")
fn erlang_time(unit: atom) -> Int
// JavaScript implementation
@external(javascript, "Date", "now")
fn js_time() -> Int
pub fn now_ms() -> Int {
// The build tool chooses the right implementation
// based on the current compile target
}
Differences from the Erlang Target
Erlang vs JavaScript Target
──────────────────────────────────────────────────
Feature │ Erlang Target │ JS Target
─────────────────┼──────────────────┼────────────────
Runtime │ BEAM VM │ Browser / Node
Concurrency │ Processes / OTP │ Promises / async
Int size │ Arbitrary │ JS number (64-bit)
Float precision │ IEEE 754 │ IEEE 754
Available libs │ Erlang ecosystem │ npm ecosystem
Output format │ .beam files │ .mjs ES modules
Practical Example — Shared Logic
// src/validation.gleam — works on BOTH targets
import gleam/string
import gleam/int
pub fn validate_email(email: String) -> Result(String, String) {
case string.contains(email, "@") && string.length(email) > 5 {
True -> Ok(email)
False -> Error("Invalid email: " <> email)
}
}
pub fn validate_age(raw: String) -> Result(Int, String) {
case int.parse(raw) {
Error(_) -> Error("Age must be a number")
Ok(age) ->
case age >= 0 && age <= 120 {
True -> Ok(age)
False -> Error("Age out of range")
}
}
}
This validation module compiles identically for the server (Erlang) and the browser (JavaScript). Write validation logic once and reuse it everywhere.
Key Points
JavaScript Target Essentials
──────────────────────────────────────────────────
1. Set target = "javascript" in gleam.toml
2. Output is ES modules (.mjs) — works with any bundler
3. @external(javascript, "module", "fn") for JS FFI
4. Provide .mjs files for browser-specific helpers
5. Lustre is the standard UI framework for browser apps
6. Pure Gleam logic compiles to both targets unchanged
7. Dual-target packages handle differences per target
The JavaScript target makes Gleam a genuine full-stack language. Your core business logic, validation, and data types live in one codebase, compiled to the right platform for each environment. The same type safety that protects your server code protects your browser code too.
