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.

Leave a Comment

Your email address will not be published. Required fields are marked *