HOME / BLOG / MLX42 in the web Tutorial
MLX42 in the web Tutorial
17 days
2025 06
Tutorial explaining all steps to publish your mlx42 code to the web for free.
MLX42 makes it possible to compile your C ready to deploy in a static webpage with WebAssembly by using https://emscripten.org/
Let's start with an awesome Makefile that I tailored for you:
Let's start with an awesome Makefile that I tailored for you:
NAME = pulga_web CC = cc CFLAGS = -O3 -Wall -Wextra -Werror -Iincludes -IMLX42/include SRCS = $(wildcard srcs/*.c) OBJS = $(SRCS:.c=.o) MLX_NATIVE_LIB = MLX42/build/libmlx42.a MLX_WEB_LIB = MLX42/build_web/libmlx42_web.a WEB = web/demo.html MLX_FLAGS = -LMLX42/build -lmlx42 -ldl -lglfw -lm -lpthread all: $(MLX_NATIVE_LIB) $(NAME) $(NAME): $(OBJS) $(CC) $(CFLAGS) $(OBJS) -o $(NAME) $(MLX_FLAGS) $(MLX_NATIVE_LIB): @cd MLX42 && cmake -B build @cd MLX42 && cmake --build build -j4 @-mv MLX42/build/libmlx42.a $(MLX_NATIVE_LIB) $(MLX_WEB_LIB): @cd MLX42 && emcmake cmake -B build_web @cd MLX42 && cmake --build build_web --parallel @-mv MLX42/build_web/libmlx42.a $(MLX_WEB_LIB) clean: rm -f $(OBJS) fclean: clean rm -f $(NAME) web: $(WEB) $(WEB): $(SRCS) $(MLX_WEB_LIB) mkdir -p web emcc -DWEB -O3 -I include -I MLX42/include -pthread $(SRCS) \ -o $(WEB) \ $(MLX_WEB_LIB) \ -s USE_GLFW=3 -s USE_WEBGL2=1 -s FULL_ES3=1 -s WASM=1 \ -s NO_EXIT_RUNTIME=1 -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \ -s ALLOW_MEMORY_GROWTH re: fclean all .PHONY: all clean fclean re $(WEB)
This Makefile comes with a cool feature which allows you to compile your project natively (for quick testing on the usual MLX window) and also for the web, when you are ready to deploy it in a website.
make # normal make, compiles your project with libmlx42 make web # web compilation with emcc , uses libml42_web
The Makefile currently produces all the files you will need for your web deployment
WEB = web/demo.html
Later I will show you how to tailor this, for now I'll demonstrate how this works with an example I prepared for you:
#include <MLX42/MLX42.h> #include <time.h> #include <stdlib.h> #include <stdio.h> #define WIDTH 800 #define HEIGHT 600 #ifdef WEB # include <emscripten.h> # include <emscripten/html5.h> #endif static mlx_t *mlx; static mlx_image_t *image; static int32_t ft_pixel(int32_t r, int32_t g, int32_t b, int32_t a) { return (r << 24 | g << 16 | b << 8 | a); } static void ft_randomize(void* param) { (void)param; for (uint32_t i = 0; i < image->width; ++i) { for (uint32_t y = 0; y < image->height; ++y) { uint32_t color = ft_pixel( rand() % 0xFF, // R rand() % 0xFF, // G rand() % 0xFF, // B rand() % 0xFF // A ); mlx_put_pixel(image, i, y, color); } } } static void ft_hook(void* param) { mlx_t* mlx = param; if (mlx_is_key_down(mlx, MLX_KEY_ESCAPE)) mlx_close_window(mlx); if (mlx_is_key_down(mlx, MLX_KEY_UP)) image->instances[0].y -= 5; if (mlx_is_key_down(mlx, MLX_KEY_DOWN)) image->instances[0].y += 5; if (mlx_is_key_down(mlx, MLX_KEY_LEFT)) image->instances[0].x -= 5; if (mlx_is_key_down(mlx, MLX_KEY_RIGHT)) image->instances[0].x += 5; } #ifdef WEB static void emscripten_main_loop(void) { mlx_loop(mlx); } #endif int main(int argc, char **argv) { (void)argc;(void)argv; if (!(mlx = mlx_init(WIDTH, HEIGHT, "MLX42", true))) { puts(mlx_strerror(mlx_errno)); return(EXIT_FAILURE); } if (!(image = mlx_new_image(mlx, 128, 128))) { mlx_close_window(mlx); puts(mlx_strerror(mlx_errno)); return(EXIT_FAILURE); } if (mlx_image_to_window(mlx, image, 0, 0) == -1) { mlx_close_window(mlx); puts(mlx_strerror(mlx_errno)); return(EXIT_FAILURE); } mlx_loop_hook(mlx, ft_randomize, mlx); mlx_loop_hook(mlx, ft_hook, mlx); #ifdef WEB emscripten_set_main_loop(emscripten_main_loop, 0, true); #else mlx_loop(mlx); #endif mlx_terminate(mlx); return EXIT_SUCCESS; }
This should be your file structure:
includes/ srcs └── main.c MLX42 ├── build ├── build_web ├── cmake ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── docs ├── include ├── lib ├── LICENSE ├── README.md ├── SECURITY.md ├── shaders ├── src ├── tests ├── tools └── web web ├── demo.html ├── demo.js └── demo.wasm Makefile
In order to understand the main.c you can refer to the official explanation in the MLX42 web directory: https://github.com/codam-coding-college/MLX42/tree/master/web

Then you can access index.html with a webserver, I use python's easy solution:
python3 -m http.server 8080

Finally let's publish it in a website.
Upload your code to a GitHub repository (it can be private)
Upload your code to a GitHub repository (it can be private)
echo "# mlx_web_example" >> README.md git init git add README.md git commit -m "first commit" git branch -M main git remote add origin https://github.com/pulgamecanica/mlx_web_example.git git push -u origin main make fclean git add . git commit -m "second commit" git push
Go To Settings

Go to Pages

Open the "Branch" dropdown

Select the "main" branch
This will automatically publish your files in the main branch to a web server. Your files will be indexed, this mean that the demo.html which was generated by emscripten will also be available. I'll explain how you can get the link...
This will automatically publish your files in the main branch to a web server. Your files will be indexed, this mean that the demo.html which was generated by emscripten will also be available. I'll explain how you can get the link...

You might see this warning if you repo is private (this is normal)

Go to the repo's settings

Toggle the box to use the GitHub Pages website URL

Toggle it again to deactivate it, we just wanted it to pre-fill the URL

Add the suffix /web/demo.html at the end of the link so it goes directly to the page!

You can visit my example website here:
https://pulgamecanica.github.io/mlx_web_example/web/demo.html
Now we will explore how you can change the website aspect because the default one is not very nice.
First let's understand the html which was generated automatically:
Starting with the "head"
https://pulgamecanica.github.io/mlx_web_example/web/demo.html
Now we will explore how you can change the website aspect because the default one is not very nice.
First let's understand the html which was generated automatically:
Starting with the "head"

In the head there is nothing too interesting, you can see some css and the tile, not that the page is not using viewport scaling, you might want to add this for phone compatibility, depending on the project specifications.
Let's start customizing the page, change the title and move the css to a style.css file.
The new head looks like this:
Let's start customizing the page, change the title and move the css to a style.css file.
The new head looks like this:
<head> <meta charset=utf-8> <meta content="text/html; charset=utf-8"http-equiv=Content-Type> <title>Pulga Page</title> <link rel="stylesheet" href="style.css"> </head>
You will need to create the style.css and put the css there, this helps dealing with the css and making changes easier.
We'll get back to the style.css later and I will give you some cool ideas.
For now let's move to the body and understand what is happening.

We have the following elements:
- image base64 encoded link <a>
- spinner <div>
- status <div>
- controls <span>
- progress <div>
- webgl canvas <div>
- terminal <textarea>
Start by removing the image link <a> and the controls <span>
- image base64 encoded link <a>
- spinner <div>
- status <div>
- controls <span>
- progress <div>
- webgl canvas <div>
- terminal <textarea>
Start by removing the image link <a> and the controls <span>

Now your body should look like this:
<div id="background"></div> <div id="main-container"> <div class="status-panel"> <div class="spinner" id="spinner"></div> <div class="emscripten" id="status">Downloading...</div> <progress hidden id="progress" max="100" value="0"></progress> </div> <div class="canvas-container"> <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1" ></canvas> </div> <button id="toggle-terminal">Toggle Terminal</button> <textarea id="output" rows="8" hidden></textarea> </div>
Also add the following to the js script:
const toggleBtn = document.getElementById("toggle-terminal"); toggleBtn.addEventListener("click", () => { outputElement.hidden = !outputElement.hidden; });
And replace the style.css with the following
/* Background styling */ body, html { margin: 0; padding: 0; height: 100%; font-family: 'Press Start 2P', monospace; background: linear-gradient(to bottom right, #1b1c2b, #0f0f0f); color: #33ff33; overflow: hidden; } #background { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; background-image: radial-gradient(#3f3f3f 1px, transparent 1px); background-size: 16px 16px; opacity: 0.1; } /* Layout container */ #main-container { display: flex; flex-direction: column; align-items: center; padding: 1rem; height: 100%; box-sizing: border-box; } /* Canvas focus */ .canvas-container { flex-grow: 1; display: flex; justify-content: center; align-items: center; padding: 1rem; } canvas { border: 4px solid #33ff33; box-shadow: 0 0 12px #33ff33; width: 90vw; height: 60vh; max-width: 1024px; background: black; } /* Status elements */ .status-panel { text-align: center; margin-bottom: 1rem; } .spinner { width: 24px; height: 24px; border: 4px solid #33ff33; border-top: 4px solid transparent; border-radius: 50%; animation: spin 1s linear infinite; margin: auto; } @keyframes spin { to { transform: rotate(360deg); } } progress { width: 60%; height: 10px; margin-top: 0.5rem; } /* Terminal output */ #output { width: 90%; max-width: 1000px; background: black; color: #33ff33; border: 2px solid #33ff33; padding: 0.5rem; resize: none; font-size: 12px; font-family: 'Courier New', monospace; margin-top: 1rem; } /* Toggle button */ #toggle-terminal { background-color: black; border: 2px solid #33ff33; color: #33ff33; font-family: 'Press Start 2P', monospace; font-size: 10px; padding: 0.5rem 1rem; margin-top: 1rem; cursor: pointer; } #toggle-terminal:hover { background-color: #111; box-shadow: 0 0 6px #33ff33; } /* Responsive tweaks */ @media (max-width: 768px) { canvas { width: 95vw; height: 50vh; } #output { font-size: 10px; } }
Finally this is how it should look like:

Finally, in order to avoid emcc from overwriting your modified html file, you should definately change in the makefile the following line:
WEB = web/demo.html
TO
WEB = web/demo.js
This way emcc will know to only generate the html
Also if your project loads anything, you will need to preload it like so:
emcc -DWEB -O3 ... ... ... -s ALLOW_MEMORY_GROWTH \ -s ALLOW_MEMORY_GROWTH \ --preload-file <your_assets_directory>
Hope you liked it! :D