---
title: "Layout / Box Model-Komponente — zenit Zig UI"
description: "Eine Abdeckungsmatrix für Boxmodell, Prozentwerte, absolute Positionierung, Flex / Grid und Clipping."
url: https://zenit.z.express/de/components/layoutbox
language: de
alternate_en: https://zenit.z.express/components/layoutbox.md
alternate_zh: https://zenit.z.express/zh/components/layoutbox.md
alternate_es: https://zenit.z.express/es/components/layoutbox.md
alternate_ja: https://zenit.z.express/ja/components/layoutbox.md
alternate_ko: https://zenit.z.express/ko/components/layoutbox.md
alternate_fr: https://zenit.z.express/fr/components/layoutbox.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Layout / Box Model

`ui.box`

Eine Abdeckungsmatrix für Boxmodell, Prozentwerte, absolute Positionierung, Flex / Grid und Clipping.

[Video](https://zenit.z.express/media/stories/layoutbox.mp4?v=4d3c4ba31b)

**BEHAVIOR CONTRACT**

Padding-Box, Content-Box, prozentuale Insets und Overflow-Clipping haben visuelle Hilfslinien.

**RECORDED INTERACTION**

Durchläuft jede Gruppe von Layout-Hilfslinien.

`examples/storybook/stories.zig:1973`

```zig
pub fn buildLayoutBoxModel(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
    _ = scope;
    const a = cx.allocator;
    const c = try col(cx, 22);
    c.style.width = .{ .px = 720 };

    // 覆盖矩阵先写在页首，截图本身即可充当人工验收 checklist。
    const coverage = try ui.box(cx, .{
        .width = .{ .px = 700 },
        .direction = .column,
        .gap = 4,
        .padding = Padding.all(14),
        .background = layout_col.surface,
        .border = .{ .width = 1, .color = layout_col.line, .radius = 8 },
    }, .{});
    try coverage.appendChild(a, try ui.text(cx, "Coverage matrix", .{
        .font_size = 13,
        .font_weight = 700,
        .color = light.color.fg_primary,
    }));
    try coverage.appendChild(a, try label(cx, "padding / border / content · px / fit / percent / grow · margin / gap"));
    try coverage.appendChild(a, try label(cx, "row / justify / align · Grid px + fr · absolute inset / percent / stretch · overflow clip"));
    try c.appendChild(a, coverage);

    // 1) CSS padding-box anatomy. Parent 700×180, asymmetric padding
    // L32/R44/T28/B20. Absolute right:0/bottom:0 must touch the padding edge
    // (Zenit 的 border 为 paint-only，所以该 edge 与本地 rect edge 重合)。
    try c.appendChild(a, try layoutSection(cx, "1. Padding-box anatomy + absolute edges", "amber = padding box · blue = content box · red = right:0 / bottom:0 · navy = content edge"));
    const anatomy = try layoutArena(cx, 700, 180, .{ .left = 32, .right = 44, .top = 28, .bottom = 20 });
    anatomy.meta.ownership.meta.test_id = "story.layoutbox.absolute.parent";

    const content_guide = try ui.box(cx, .{
        .position = .absolute,
        .width = .{ .grow = .{} },
        .height = .{ .grow = .{} },
        .justify = .center,
        .align_items = .center,
        .background = layout_col.content,
        .border = .{ .width = 1, .color = ui.Color.rgba(96, 165, 250, 255), .radius = 4 },
    }, .{});
    (try content_guide.style.ensureExtFallible(a)).inset = .{
        .left = .{ .px = 32 },
        .right = .{ .px = 44 },
        .top = .{ .px = 28 },
        .bottom = .{ .px = 20 },
    };
    content_guide.meta.ownership.meta.test_id = "story.layoutbox.absolute.content-guide";
    try content_guide.appendChild(a, try ui.text(cx, "content 624 × 132", .{
        .font_size = 12,
        .font_weight = 650,
        .color = ui.Color.rgba(30, 64, 175, 255),
    }));
    try anatomy.appendChild(a, content_guide);

    const origin = try layoutBadge(cx, "0,0", 38, 26, layout_col.stretch);
    origin.style.position = .absolute;
    (try origin.style.ensureExtFallible(a)).inset = .{ .left = .{ .px = 0 }, .top = .{ .px = 0 } };
    origin.meta.ownership.meta.test_id = "story.layoutbox.absolute.origin";
    try anatomy.appendChild(a, origin);

    const outer_edge = try layoutBadge(cx, "edge", 44, 30, layout_col.absolute);
    outer_edge.style.position = .absolute;
    (try outer_edge.style.ensureExtFallible(a)).inset = .{ .right = .{ .px = 0 }, .bottom = .{ .px = 0 } };
    outer_edge.meta.ownership.meta.test_id = "story.layoutbox.absolute.outer-edge";
    try anatomy.appendChild(a, outer_edge);

    const content_edge = try layoutBadge(cx, "content", 58, 30, layout_col.flow);
    content_edge.style.position = .absolute;
    (try content_edge.style.ensureExtFallible(a)).inset = .{ .right = .{ .px = 44 }, .bottom = .{ .px = 20 } };
    content_edge.meta.ownership.meta.test_id = "story.layoutbox.absolute.content-edge";
    try anatomy.appendChild(a, content_edge);
    try c.appendChild(a, anatomy);
    try c.appendChild(a, try label(cx, "Expected: content=(32,28 624×132), red right/bottom gap=0, navy right gap=44 bottom gap=20"));

    // 2) Percentage resolution and double-sided grow stretch.
    try c.appendChild(a, try layoutSection(cx, "2. Percentage inset + double-sided stretch", "Percentages resolve against the full padding box; left+right/top+bottom constrain grow size."));
    const inset_row = try row(cx, 16);

    const percent_col = try col(cx, 6);
    try percent_col.appendChild(a, try label(cx, "left:25% · top:50% → (85.5, 75)"));
    const percent_parent = try layoutArena(cx, 342, 150, Padding.all(20));
    percent_parent.meta.ownership.meta.test_id = "story.layoutbox.percent.parent";
    const vguide = try ui.box(cx, .{
        .position = .absolute,
        .width = .{ .px = 1 },
        .height = .{ .grow = .{} },
        .background = layout_col.guide,
    }, .{});
    (try vguide.style.ensureExtFallible(a)).inset = .{ .left = .{ .px = 85.5 } };
    try percent_parent.appendChild(a, vguide);
    const hguide = try ui.box(cx, .{
        .position = .absolute,
        .width = .{ .grow = .{} },
        .height = .{ .px = 1 },
        .background = layout_col.guide,
    }, .{});
    (try hguide.style.ensureExtFallible(a)).inset = .{ .top = .{ .px = 75 } };
    try percent_parent.appendChild(a, hguide);
    const percent_marker = try layoutBadge(cx, "25/50", 52, 28, layout_col.absolute);
    percent_marker.style.position = .absolute;
    (try percent_marker.style.ensureExtFallible(a)).inset = .{
        .left = .{ .percent = 25 },
        .top = .{ .percent = 50 },
    };
    percent_marker.meta.ownership.meta.test_id = "story.layoutbox.percent.marker";
    try percent_parent.appendChild(a, percent_marker);
    try percent_col.appendChild(a, percent_parent);
    try inset_row.appendChild(a, percent_col);

    const stretch_col = try col(cx, 6);
    try stretch_col.appendChild(a, try label(cx, "L14 / R22 / T18 / B12 + grow"));
    const stretch_parent = try layoutArena(cx, 342, 150, .{ .left = 26, .right = 12, .top = 10, .bottom = 22 });
    stretch_parent.meta.ownership.meta.test_id = "story.layoutbox.stretch.parent";
    const stretch_child = try ui.box(cx, .{
        .position = .absolute,
        .width = .{ .grow = .{} },
        .height = .{ .grow = .{} },
        .justify = .center,
        .align_items = .center,
        .background = layout_col.stretch,
        .border = .{ .radius = 5 },
    }, .{});
    (try stretch_child.style.ensureExtFallible(a)).inset = .{
        .left = .{ .px = 14 },
        .right = .{ .px = 22 },
        .top = .{ .px = 18 },
        .bottom = .{ .px = 12 },
    };
    stretch_child.meta.ownership.meta.test_id = "story.layoutbox.stretch.child";
    try stretch_child.appendChild(a, try ui.text(cx, "306 × 120", .{
        .font_size = 12,
        .font_weight = 650,
        .color = layout_col.ink_on_color,
    }));
    try stretch_parent.appendChild(a, stretch_child);
    try stretch_col.appendChild(a, stretch_parent);
    try inset_row.appendChild(a, stretch_col);
    try c.appendChild(a, inset_row);

    // 3) Flex and Grid both keep absolute children out of normal placement,
    // while using the same padding-box containing block for inset resolution.
    try c.appendChild(a, try layoutSection(cx, "3. Flex vs Grid: in-flow content and out-of-flow absolute", "Flow items start at padding.left; the red absolute marker consumes no gap, track, or flex space."));
    const engines = try row(cx, 16);

    const flex_col = try col(cx, 6);
    try flex_col.appendChild(a, try label(cx, "Flex row · padding 20 · gap 12 · margins on B"));
    const flex_parent = try layoutArena(cx, 342, 170, Padding.all(20));
    flex_parent.style.direction = .row;
    flex_parent.style.gap = 12;
    flex_parent.style.align_items = .center;
    flex_parent.meta.ownership.meta.test_id = "story.layoutbox.flex.parent";
    const flow_a = try layoutBadge(cx, "A 64", 64, 56, layout_col.flow);
    flow_a.meta.ownership.meta.test_id = "story.layoutbox.flex.a";
    try flex_parent.appendChild(a, flow_a);
    const flow_b = try layoutBadge(cx, "B +m", 64, 56, layout_col.flow_alt);
    flow_b.style.margin = .{ .left = 8, .right = 6, .top = 0, .bottom = 0 };
    flow_b.meta.ownership.meta.test_id = "story.layoutbox.flex.b";
    try flex_parent.appendChild(a, flow_b);
    const flow_grow = try layoutBadge(cx, "grow", 10, 56, ui.Color.rgba(124, 58, 237, 255));
    flow_grow.style.width = .{ .grow = .{} };
    flow_grow.style.margin.right = 10;
    flow_grow.meta.ownership.meta.test_id = "story.layoutbox.flex.grow";
    try flex_parent.appendChild(a, flow_grow);
    const flex_abs = try layoutBadge(cx, "abs", 34, 34, layout_col.absolute);
    flex_abs.style.position = .absolute;
    (try flex_abs.style.ensureExtFallible(a)).inset = .{ .right = .{ .px = 0 }, .top = .{ .px = 0 } };
    flex_abs.meta.ownership.meta.test_id = "story.layoutbox.flex.absolute";
    try flex_parent.appendChild(a, flex_abs);
    try flex_col.appendChild(a, flex_parent);
    try engines.appendChild(a, flex_col);

    const grid_col = try col(cx, 6);
    try grid_col.appendChild(a, try label(cx, "Grid · 70px / 1fr / 2fr · gap 8"));
    const grid_a = try layoutBadge(cx, "70px", 10, 52, layout_col.flow);
    grid_a.style.width = .{ .grow = .{} };
    grid_a.meta.ownership.meta.test_id = "story.layoutbox.grid.a";
    const grid_b = try layoutBadge(cx, "1fr", 10, 52, layout_col.flow_alt);
    grid_b.style.width = .{ .grow = .{} };
    grid_b.meta.ownership.meta.test_id = "story.layoutbox.grid.b";
    const grid_c = try layoutBadge(cx, "2fr", 10, 52, ui.Color.rgba(124, 58, 237, 255));
    grid_c.style.width = .{ .grow = .{} };
    grid_c.meta.ownership.meta.test_id = "story.layoutbox.grid.c";
    const grid_abs = try layoutBadge(cx, "abs", 34, 34, layout_col.absolute);
    grid_abs.style.position = .absolute;
    (try grid_abs.style.ensureExtFallible(a)).inset = .{ .right = .{ .px = 0 }, .bottom = .{ .px = 0 } };
    grid_abs.meta.ownership.meta.test_id = "story.layoutbox.grid.absolute";
    const grid_parent = try ui.grid(cx, .{
        .columns = &.{ .{ .px = 70 }, .{ .fr = 1 }, .{ .fr = 2 } },
        .rows = &.{.{ .px = 52 }},
        .column_gap = 8,
        .width = .{ .px = 342 },
        .height = .{ .px = 170 },
        .padding = Padding.all(20),
        .background = layout_col.padding,
        .border = .{ .width = 1, .color = layout_col.line, .radius = 8 },
        .align_items = .center,
    }, .{ grid_a, grid_b, grid_c, grid_abs });
    grid_parent.meta.ownership.meta.test_id = "story.layoutbox.grid.parent";
    try grid_col.appendChild(a, grid_parent);
    try engines.appendChild(a, grid_col);
    try c.appendChild(a, engines);

    // 4) Sizing modes share the parent's content width, while the parent still
    // owns padding. This row makes px/percent/fit/grow contributions visible.
    try c.appendChild(a, try layoutSection(cx, "4. Sizing modes inside one Flex row", "Parent 700 wide, padding 20 → content 660. The 20% item is 132 wide; grow receives the remainder."));
    const sizing_parent = try ui.box(cx, .{
        .width = .{ .px = 700 },
        .height = .{ .px = 100 },
        .direction = .row,
        .align_items = .center,
        .gap = 10,
        .padding = Padding.all(20),
        .background = layout_col.content,
        .border = .{ .width = 1, .color = layout_col.line, .radius = 8 },
    }, .{});
    sizing_parent.meta.ownership.meta.test_id = "story.layoutbox.sizing.parent";
    const sizing_px = try layoutBadge(cx, "px 80", 80, 44, layout_col.flow);
    sizing_px.meta.ownership.meta.test_id = "story.layoutbox.sizing.px";
    try sizing_parent.appendChild(a, sizing_px);
    const sizing_percent = try layoutBadge(cx, "20%", 10, 44, layout_col.flow_alt);
    sizing_percent.style.width = .{ .percent = 20 };
    sizing_percent.meta.ownership.meta.test_id = "story.layoutbox.sizing.percent";
    try sizing_parent.appendChild(a, sizing_percent);
    const sizing_fit = try layoutBadge(cx, "fit content", 10, 44, ui.Color.rgba(124, 58, 237, 255));
    sizing_fit.style.width = .{ .fit = .{} };
    sizing_fit.meta.ownership.meta.test_id = "story.layoutbox.sizing.fit";
    try sizing_parent.appendChild(a, sizing_fit);
    const sizing_grow = try layoutBadge(cx, "grow", 10, 44, layout_col.stretch);
    sizing_grow.style.width = .{ .grow = .{} };
    sizing_grow.meta.ownership.meta.test_id = "story.layoutbox.sizing.grow";
    try sizing_parent.appendChild(a, sizing_grow);
    try c.appendChild(a, sizing_parent);

    // 5) Main-axis distribution and cross-axis alignment. Each sample uses the
    // same content width, so start/center/end drift is immediately visible.
    try c.appendChild(a, try layoutSection(cx, "5. Flex distribution: justify + align + gap", "Three identical 220×92 parents; only justify changes. Cross axis stays centered."));
    const justify_row = try row(cx, 16);
    inline for (.{ ui.JustifyContent.start, .center, .end }, .{ "start", "center", "end" }) |justify, name| {
        const sample_col = try col(cx, 5);
        try sample_col.appendChild(a, try label(cx, name));
        const sample = try ui.box(cx, .{
            .width = .{ .px = 220 },
            .height = .{ .px = 92 },
            .direction = .row,
            .justify = justify,
            .align_items = .center,
            .gap = 8,
            .padding = Padding.all(12),
            .background = layout_col.content,
            .border = .{ .width = 1, .color = layout_col.line, .radius = 8 },
        }, .{});
        try sample.appendChild(a, try layoutBadge(cx, "1", 36, 30, layout_col.flow));
        try sample.appendChild(a, try layoutBadge(cx, "2", 36, 30, layout_col.flow_alt));
        try sample_col.appendChild(a, sample);
        try justify_row.appendChild(a, sample_col);
    }
    try c.appendChild(a, justify_row);

    // 6) Overflow is part of the box model contract. Same overflowing absolute
    // child in both arenas; only the left parent clips at its padding-box edge.
    try c.appendChild(a, try layoutSection(cx, "6. Overflow clipping", "The rose block starts at (280,55), size 100×60. Left clips; right remains visible."));
    const overflow_row = try row(cx, 16);
    inline for (.{ true, false }, .{ "overflow_hidden", "overflow_visible" }) |clipped, name| {
        const sample_col = try col(cx, 5);
        try sample_col.appendChild(a, try label(cx, name));
        const sample = try ui.box(cx, .{
            .width = .{ .px = 342 },
            .height = .{ .px = 110 },
            .position = .relative,
            .padding = Padding.all(12),
            .background = layout_col.surface,
            .border = .{ .width = 1, .color = layout_col.line, .radius = 8 },
            .overflow_hidden = clipped,
        }, .{});
        const overflow_child = try layoutBadge(cx, "100 × 60", 100, 60, layout_col.absolute);
        overflow_child.style.position = .absolute;
        (try overflow_child.style.ensureExtFallible(a)).inset = .{ .left = .{ .px = 280 }, .top = .{ .px = 55 } };
        try sample.appendChild(a, overflow_child);
        try sample_col.appendChild(a, sample);
        try overflow_row.appendChild(a, sample_col);
    }
    try c.appendChild(a, overflow_row);

    return c;
}
```

[Quellcode auf GitHub ansehenexamples/storybook/stories.zig:1973](https://github.com/version-next/zenit/blob/HEAD/examples/storybook/stories.zig#L1973)

Der unveränderte Quellcode der Story aus dem Storybook (zenit 5f9add5+wip 2026-09-30). `W` steht für `ui.widgets`; `col` / `row` / `label` sind kleine Layout-Helfer des Storybooks.
