Rust egui Grid Layout — Build an Aligned Contact Form (Ep 8)
Video: Rust egui Grid Layout — Build an Aligned Contact Form (Ep 8) by Taught by Celeste AI - AI Coding Coach
egui::Grid::new(id).num_columns(2).show(ui, |ui| ...)— labels and inputs that line up in clean columns, no manual width tuning.
The horizontal/vertical nesting from Episode 7 works for two-column page layouts. It does not work for forms — labels and inputs that need to line up in columns across multiple rows. For that you want egui::Grid.
Grid is one of the few non-immediate widgets in egui — it is a small layout engine that measures cell widths over multiple frames and aligns them. The result is the kind of clean form layout you would expect from a desktop app, with one widget per call.
What we are building
A contact form with four labelled inputs (Name, Email, Phone, Subject), a Submit button, and a confirmation panel that appears after Submit and shows what the user typed — also in a grid.
The script
use eframe::egui;
pub struct MyApp {
name: String,
email: String,
phone: String,
subject: String,
submitted: bool,
}
impl Default for MyApp {
fn default() -> Self {
Self {
name: String::new(),
email: String::new(),
phone: String::new(),
subject: String::new(),
submitted: false,
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Contact Form");
ui.separator();
egui::Grid::new("contact_grid")
.num_columns(2)
.spacing([40.0, 8.0])
.show(ui, |ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut self.name);
ui.end_row();
ui.label("Email:");
ui.text_edit_singleline(&mut self.email);
ui.end_row();
ui.label("Phone:");
ui.text_edit_singleline(&mut self.phone);
ui.end_row();
ui.label("Subject:");
ui.text_edit_singleline(&mut self.subject);
ui.end_row();
});
ui.separator();
if ui.button("Submit").clicked() {
self.submitted = true;
}
if self.submitted {
ui.separator();
ui.label("Submitted!");
egui::Grid::new("summary_grid")
.num_columns(2)
.spacing([40.0, 4.0])
.show(ui, |ui| {
ui.label("Name:");
ui.label(&self.name);
ui.end_row();
ui.label("Email:");
ui.label(&self.email);
ui.end_row();
ui.label("Phone:");
ui.label(&self.phone);
ui.end_row();
ui.label("Subject:");
ui.label(&self.subject);
ui.end_row();
});
}
});
}
}
Two grids. Same shape: label in column 1, value in column 2, end_row() after each row.
egui::Grid::new
egui::Grid::new("contact_grid")
.num_columns(2)
.spacing([40.0, 8.0])
.show(ui, |ui| {
// rows
});
Builder pattern.
Grid::new(id)— every grid needs a unique string ID. egui uses it to remember per-frame layout measurements (column widths). Two grids with the same ID would interfere..num_columns(2)— declare how many columns the grid has. Necessary for the layout engine to know when a row ends..spacing([horizontal, vertical])— gap between adjacent cells.[40.0, 8.0]means 40 pixels horizontal between columns, 8 pixels vertical between rows..show(ui, |ui| { ... })— the closure where you add the cells.
Other useful builder methods: .striped(true) (alternating row backgrounds), .min_col_width(px) and .max_col_width(px) for per-column constraints.
Adding cells
ui.label("Name:");
ui.text_edit_singleline(&mut self.name);
ui.end_row();
Inside the grid closure, each widget call places one cell. After num_columns widgets, you call ui.end_row() to start a new row.
If you forget end_row(), widgets keep flowing into the same row and the grid stops behaving correctly. If you call it twice in a row, you get an empty row.
You can put any widget in any cell — a button, a slider, a nested grid. The cell takes the widget's natural size; the grid aligns the cell positions across rows.
Two grids in one panel
egui::Grid::new("contact_grid").show(...);
egui::Grid::new("summary_grid").show(...);
Different IDs — contact_grid and summary_grid — because egui uses the ID to track measurements per grid. Two grids with the same ID would cross-contaminate width calculations.
Conditional grid
if self.submitted {
ui.separator();
ui.label("Submitted!");
egui::Grid::new("summary_grid").show(ui, |ui| { ... });
}
The summary grid only appears after the user clicks Submit. Standard immediate-mode pattern: wrap the widget in an if. The first frame after Submit, the summary section appears; on every subsequent frame, the if is true and it stays visible.
In a real form you would also want a "Reset" button that flips submitted back to false:
if ui.button("Reset").clicked() {
self.submitted = false;
self.name.clear();
self.email.clear();
// ...
}
Why Grid over horizontal + vertical
You could write the contact form using horizontal for each row:
ui.horizontal(|ui| { ui.label("Name:"); ui.text_edit_singleline(&mut self.name); });
ui.horizontal(|ui| { ui.label("Email:"); ui.text_edit_singleline(&mut self.email); });
ui.horizontal(|ui| { ui.label("Phone:"); ui.text_edit_singleline(&mut self.phone); });
ui.horizontal(|ui| { ui.label("Subject:"); ui.text_edit_singleline(&mut self.subject); });
It compiles. It looks roughly right. But the inputs do not align across rows because ui.label("Name:") is narrower than ui.label("Subject:") — and each horizontal block measures its own width independently.
Grid solves this. It measures column widths globally and pads each label cell to the widest one. The text inputs line up vertically. That alignment is the point.
When Grid does not apply
- Variable column counts per row. A grid expects
num_columnsper row exactly. If row A has 2 cells and row B has 4, usehorizontal+verticalinstead. - Spanning columns. egui's
Griddoes not have colspan. For complex layouts with merged cells, switch to a custom layout or to a table widget likeegui_extras::TableBuilder. - Very long forms with scrolling.
Gridmeasures all cells every frame; for thousands of rows, use a virtualised table.
For 2–10 row forms with 2 columns, Grid is the right tool.
Running it
cargo run. Type into the four fields. Click Submit. The "Submitted!" label appears, followed by a summary grid with the values you typed.
Try clicking Submit before typing anything — the summary appears with empty values. In a real form you would validate first.
Common mistakes
Forgetting .num_columns(N). The grid still works but column-width measurement may be off.
Using the same ID for two grids. They will fight over column widths and the layout will jitter.
Forgetting ui.end_row(). Widgets keep flowing into the same row, eventually wrapping into the next row but at the wrong cell positions.
Adding fewer cells than num_columns before end_row. Empty cells appear in the row. Either add placeholder widgets or call ui.end_row() after the right number of cells.
What's next
Next episode: side panels. A real-life pattern: a fixed-width navigation panel docked to the left of the window, with the main content filling the rest. Built using egui::SidePanel, the higher-level alternative to nested horizontal+vertical.
Recap
egui::Grid::new(id).num_columns(N).show(ui, |ui| ...) for aligned multi-row layouts. ui.end_row() after every N widgets. Each grid needs a unique ID. Use spacing([h, v]) to control gaps. Wrap conditional grids in if statements for immediate-mode show/hide.
Next episode: side panels. See you in the next one.