---
title: "Button 컴포넌트 — zenit Zig UI"
description: "기본, 보조, 위험 동작, 링크형, 아이콘 전용 버튼.variant × size, loading, disabled, block, 앞쪽 아이콘, 키보드 활성화…"
url: https://zenit.z.express/ko/components/button
language: ko
alternate_en: https://zenit.z.express/components/button.md
alternate_zh: https://zenit.z.express/zh/components/button.md
alternate_es: https://zenit.z.express/es/components/button.md
alternate_ja: https://zenit.z.express/ja/components/button.md
alternate_fr: https://zenit.z.express/fr/components/button.md
alternate_de: https://zenit.z.express/de/components/button.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Button

`ui.widgets.Button`

기본, 보조, 위험 동작, 링크형, 아이콘 전용 버튼.

[Video](https://zenit.z.express/media/stories/button.mp4?v=6203a01415)

**BEHAVIOR CONTRACT**

variant × size, loading, disabled, block, 앞쪽 아이콘, 키보드 활성화를 하나의 모델로 다룹니다.

**RECORDED INTERACTION**

variant × size 매트릭스를 훑은 뒤 Primary에서 hover와 누르기를 보여 줍니다.

`examples/storybook/stories.zig:29`

```zig
pub fn buildButton(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
    const a = cx.allocator;
    const icons = ui.assets.common;
    const c = try col(cx, 16);

    const variant_specs = [_]struct { v: W.ButtonVariant, t: []const u8 }{
        .{ .v = .primary, .t = "Primary" },
        .{ .v = .secondary, .t = "Secondary" },
        .{ .v = .ghost, .t = "Ghost" },
        .{ .v = .danger, .t = "Danger" },
        .{ .v = .link, .t = "Link" },
    };
    const size_specs = [_]struct { s: W.ButtonSize, t: []const u8 }{
        .{ .s = .xs, .t = "XS" },
        .{ .s = .sm, .t = "SM" },
        .{ .s = .md, .t = "MD" },
        .{ .s = .lg, .t = "LG" },
    };

    // ── variant × size 全矩阵 ──
    try c.appendChild(a, try label(cx, "Variants × Sizes"));
    const matrix = try col(cx, 8);
    inline for (variant_specs) |vs| {
        const r = try row(cx, 10);
        inline for (size_specs) |ss| {
            try r.appendChild(a, try W.Button(.{
                .label = vs.t,
                .variant = vs.v,
                .size = ss.s,
            }).mount(scope, cx));
        }
        try matrix.appendChild(a, r);
    }
    try c.appendChild(a, matrix);

    // ── 带前置图标（leading icon）每个 variant ──
    try c.appendChild(a, try label(cx, "With leading icon"));
    const icon_row = try row(cx, 10);
    inline for (variant_specs) |vs| {
        try icon_row.appendChild(a, try W.Button(.{
            .label = vs.t,
            .variant = vs.v,
            .icon_asset = icons.plus,
        }).mount(scope, cx));
    }
    try c.appendChild(a, icon_row);

    // ── leading icon × sizes（图标随尺寸缩放）──
    try c.appendChild(a, try label(cx, "Leading icon × sizes"));
    const icon_size_row = try row(cx, 10);
    inline for (size_specs) |ss| {
        try icon_size_row.appendChild(a, try W.Button(.{
            .label = "Download",
            .variant = .secondary,
            .size = ss.s,
            .icon_asset = icons.folder,
            .icon_size = cx.tokens.control.get(ss.s).icon_size,
        }).mount(scope, cx));
    }
    try c.appendChild(a, icon_size_row);

    // ── icon-only（仅图标）每个 variant ──
    try c.appendChild(a, try label(cx, "Icon only"));
    const icon_only_row = try row(cx, 10);
    inline for (variant_specs) |vs| {
        try icon_only_row.appendChild(a, try W.Button(.{
            .variant = vs.v,
            .icon_only = true,
            .icon_asset = icons.star,
        }).mount(scope, cx));
    }
    try c.appendChild(a, icon_only_row);

    // ── icon-only × sizes ──
    try c.appendChild(a, try label(cx, "Icon only × sizes"));
    const icon_only_sizes = try row(cx, 10);
    inline for (size_specs) |ss| {
        try icon_only_sizes.appendChild(a, try W.Button(.{
            .variant = .secondary,
            .icon_only = true,
            .icon_asset = icons.search,
            .size = ss.s,
            .icon_size = cx.tokens.control.get(ss.s).icon_size,
        }).mount(scope, cx));
    }
    try c.appendChild(a, icon_only_sizes);

    // ── loading（加载中）每个 variant ──
    try c.appendChild(a, try label(cx, "Loading"));
    const loading_row = try row(cx, 10);
    inline for (variant_specs) |vs| {
        try loading_row.appendChild(a, try W.Button(.{
            .label = "Saving…",
            .variant = vs.v,
            .loading = true,
        }).mount(scope, cx));
    }
    try c.appendChild(a, loading_row);

    // ── loading × sizes（spinner 随尺寸缩放）──
    try c.appendChild(a, try label(cx, "Loading × sizes"));
    const loading_sizes = try row(cx, 10);
    inline for (size_specs) |ss| {
        try loading_sizes.appendChild(a, try W.Button(.{
            .label = "Loading",
            .variant = .primary,
            .size = ss.s,
            .loading = true,
        }).mount(scope, cx));
    }
    try c.appendChild(a, loading_sizes);

    // ── 状态：normal / disabled / disabled+icon / loading+icon-only ──
    try c.appendChild(a, try label(cx, "States"));
    const states = try row(cx, 10);
    try states.appendChild(a, try W.Button(.{ .label = "Normal" }).mount(scope, cx));
    try states.appendChild(a, try W.Button(.{ .label = "Disabled", .disabled = true }).mount(scope, cx));
    try states.appendChild(a, try W.Button(.{ .label = "Disabled", .disabled = true, .icon_asset = icons.plus }).mount(scope, cx));
    try states.appendChild(a, try W.Button(.{ .label = "Loading", .loading = true }).mount(scope, cx));
    try states.appendChild(a, try W.Button(.{ .icon_only = true, .icon_asset = icons.x_close, .loading = true, .variant = .secondary }).mount(scope, cx));
    try c.appendChild(a, states);

    // ── block（块级，宽度填充）──
    try c.appendChild(a, try label(cx, "Block (full width)"));
    const block_col = try col(cx, 8);
    block_col.style.width = .{ .px = 280 };
    try block_col.appendChild(a, try W.Button(.{ .label = "Block primary", .block = true }).mount(scope, cx));
    try block_col.appendChild(a, try W.Button(.{ .label = "Block + icon", .block = true, .variant = .secondary, .icon_asset = icons.check }).mount(scope, cx));
    try c.appendChild(a, block_col);

    return c;
}
```

[GitHub에서 소스 보기examples/storybook/stories.zig:29](https://github.com/version-next/zenit/blob/HEAD/examples/storybook/stories.zig#L29)

Storybook에 있는 이 story의 원본 코드입니다(zenit 5f9add5+wip 2026-09-30). `W`는 `ui.widgets`이며, `col` / `row` / `label`은 Storybook 내부의 작은 레이아웃 헬퍼입니다.

### ButtonProps

`ui.widgets.Button`

[src/ui/components/button/mod.zig:34](https://github.com/version-next/zenit/blob/HEAD/src/ui/components/button/mod.zig#L34)

```zig
fn Button(props: ButtonProps) ButtonBuilder
```

| 필드 | 타입 | 기본값 | 설명 |
| --- | --- | --- | --- |
| `label` | `?[]const u8` | `null` | 标签文本 |
| `variant` | `ButtonVariant` `.primary` `.secondary` `.ghost` `.danger` `.link` `.field` | `.primary` | 变体 |
| `size` | `ButtonSize` | `.md` | 尺寸 |
| `icon_only` | `bool` | `false` | 图标模式（只显示图标） |
| `loading` | `bool` | `false` | 加载中 |
| `disabled` | `bool` | `false` | 禁用 |
| `block` | `bool` | `false` | 块级（宽度填充） 非 block Button 保持 intrinsic 宽度：父容器交叉轴为 stretch（默认）时按 start 排而不被拉伸；父显式 align\_items=.center/.end 时照常居中/靠尾。 |
| `on_click` | `?core.HandlerRef` | `null` | 点击 handler |
| `icon` | `?u32` | `null` | 图标纹理 ID（放在 label 文本左侧） |
| `icon_asset` | `?svg_assets.Asset` | `null` | SVG 图标资源（优先于 texture icon） |
| `icon_size` | `?f32` | `null` | 图标尺寸（null = 跟随 control metrics 的 icon\_size；应 ≤ 行高） |
| `icon_tint` | `?Color` | `null` | 图标 tint 颜色（默认跟随 textColor） |
| `on_event` | `?GenericEventCallback` | `null` | 通用事件处理（支持 hover/key 等复合事件） |
| `event_context` | `?*anyopaque` | `null` | 事件上下文 |
| **样式覆盖** | | | |
| `style` | `StyleOverride` | `.{}` | 默认态样式覆盖 |
| `hover_style` | `?StyleOverride` | `null` | hover 态样式覆盖 |
| `pressed_style` | `?StyleOverride` | `null` | pressed 态样式覆盖 |

### \*Node

`ui.widgets.Button`

mount는 `*Node`을(를) 직접 반환합니다. 부모 노드에 append하면 됩니다.
