Okay, so I have been wanting to get a deeper grasp and understanding of how frontend frameworks, web servers and caches ect… Work underneath the hood? Being inspired by GitHub - nicbarker/clay: High performance UI layout library in C.. C layout library, I have decided to attempt to build my own frontend framework from scratch in zig, which compiles to wasm. Below is an example the framework in use, the aim to be developer friendly and somewhat related to other frontend frameworks. Curious what all of your takes are on this?
Main goals
only zig, no tsx or transpiling ect…
should be somewhate zig idiomatic
highly flexible, ie no file naming syntax like +page.server.ts, or +layout.svelte, ect…
easy to read and maintain, should be explicit.
composable and modular
const std = @import("std");
// Import the Fabric UI framework
const Fabric = @import("fabric");
const Theme = @import("../../main.zig").theme;
// Select component
const Select = @import("../../lib/Select.zig").Select;
// Signals are used to hold reactive state that automatically triggers UI updates
const Signal = Fabric.Signal;
// Fabric provides multiple component types depending on how dynamic the content is:
// Static: never updates after initial render
const Static = Fabric.Static;
// Pure: updates when its props change
const Pure = Fabric.Pure;
// Dynamic: binds directly to signals, auto-updates when signal changes
const Dynamic = Fabric.Dynamic;
// Binded: directly binds to low-level HTML/DOM elements, for two-way binding
const Binded = Fabric.Binded;
// Declare colors
var primary: [4]f32 = undefined;
var secondary: [4]f32 = undefined;
var btn_color: [4]f32 = undefined;
var border_color: [4]f32 = undefined;
var alternate_tint: [4]f32 = undefined;
var text_color: [4]f32 = undefined;
// --- DATA STRUCTURES ---
// Each project contains title, author, and image fields
const Project = struct {
title: []const u8,
author: []const u8,
img: []const u8,
};
// Reactive list of projects to be displayed
var projects: Signal([]Project) = undefined;
// UI State flags (dialog and filter visibility toggles)
var show_dialog: Signal(bool) = undefined;
var show_filter_menu: Signal(bool) = undefined;
// Filter dropdown list data structure
const Item = struct {
label: []const u8,
value: void = {}, // Placeholder for value if needed later
};
// List of filter options
var filter_list: std.ArrayList(Item) = undefined;
// Select component instance
var select: Select(Item, &filter_list) = undefined;
// Input element reference for search bar (binded allows direct interaction with DOM)
var search_bar: Fabric.Element = Fabric.Element{};
// --- INITIALIZATION ---
pub fn init() void {
// Initialize colors from the Theme
primary = Theme.instance.primary;
secondary = Theme.instance.secondary;
btn_color = Theme.instance.btn_color;
border_color = Theme.instance.border_color;
text_color = Theme.instance.text_color;
alternate_tint = Theme.instance.alternate_tint;
// Initialize projects signal with empty list
projects.initv2(&.{});
// Initialize boolean toggle flags
show_dialog.initv2(false);
show_filter_menu.initv2(false);
// Fetch project data from server API and update state when response arrives
// takes a url, a argument for the callback, a callback function for the response, and details on the request
Fabric.Kit.fetchWithParams("http://localhost:8443/projects", {}, setProjectsList, .{
.method = "GET",
.credentials = "include", // Attach cookies/session
.headers = .{ .content_type = "text/html" }, // Adjust if your backend returns JSON
});
// Initialize filter options and dropdown menu
filter_list = std.ArrayList(Item).init(Fabric.lib.allocator_global);
filter_list.appendSlice(&.{ Item{ .label = "Activity" }, Item{ .label = "Name" } }) catch return;
select.init(&Fabric.lib.allocator_global, "Sort", null);
// Mount page using Fabric's page mounting system
// Still thinking about how I want to handle creating pages?
// current the page function takes a source and uses this as the routes ie
// /routes/app/users -> /app/users
// /routes/auth/login -> /auth/login
// render function is passed and stored for rerendering and reconciliation
// can pass null or a deinit function when a page is destroyed
Fabric.Page(@src(), render, null, .{
.width = .percent(1),
.height = .percent(1),
});
}
// --- API RESPONSE HANDLER ---
// Callback that will receive the HTTP response
fn setProjectsList(_: void, resp: Fabric.Kit.Response) void {
Fabric.println("{s}", .{resp.body});
}
// --- REUSABLE COMPONENTS ---
// Define a reusable Card component
inline fn Card() Fabric.Component {
return Static.FlexBox(.{
.border_radius = .all(8),
.border_color = border_color,
.padding = .all(8),
.height = .fixed(200),
.width = .grow,
.direction = .column,
.border_thickness = .all(1),
.child_gap = 12,
});
}
// --- EVENT HANDLERS ---
// Simple functions to toggle UI state (used as button handlers)
fn openDialog() void {
show_dialog.toggle();
}
fn openFilter() void {
show_filter_menu.toggle();
}
// --- MAIN RENDER FUNCTION ---
pub fn render() void {
// Top-level vertical FlexBox container
Static.FlexBox(.{
.width = .percent(1),
.height = .percent(1),
.direction = .column,
.child_alignment = .{ .x = .start, .y = .center },
})({
Static.FlexBox(.{
.width = .percent(0.5),
.child_alignment = .{ .x = .between, .y = .center },
.padding = .horizontal(12),
})({
// Search bar input, binded to DOM for two-way data binding
Binded.Input(&search_bar, .{ .string = .{ .default = "Search...", .required = true } }, .{
.background = primary,
.text_color = text_color,
.outline = .none,
.font_size = 16,
.border_color = border_color,
.border_radius = .all(8),
.border_thickness = .all(1),
.padding = .{ .left = 12, .right = 12, .bottom = 10, .top = 10 },
.width = .percent(0.7),
});
Static.FlexBox(.{
.width = .percent(0.3),
.child_gap = 4,
})({
// Render dropdown select component
Static.FlexBox(.{
.width = .percent(0.6),
})({
select.render();
});
// Add Button that opens the dialog (plus icon)
Static.Button(openDialog, .{
.border_color = border_color,
.border_thickness = .all(1),
.border_radius = .all(6),
.width = .fixed(41),
.height = .fixed(41),
})({
Static.Icon("bi bi-plus-lg", .{
.text_color = text_color,
.font_size = 16,
});
});
});
});
// Render each project in projects list
for (projects.get()) |project| {
Card()({
Static.FlexBox(.{
.direction = .column,
})({
Static.Text(project.title, .{});
Static.Text(project.author, .{});
Static.Text(project.img, .{});
});
});
}
});
}