Behavior in the SDK.
Design in the look.

Lookless controls own interaction. Templates render them. Look crates provide a visual system.

You can customize SDK controls with easy-to-use templating or template overrides. Keep the behavior; change the presentation.

Architecture

SDK controls handle state, focus, input, and events. Templates render them. Look crates configure templates and styling, so different designs share the same control behavior. A Look is optional: the SDK’s native Rust baseline supplies complete vanilla styling, and Look stylesheets inherit it. See the shared baseline.

The following are integration snippets for an initialized application. cx is the owning view’s context. The first snippet assumes an owner with a save_requests: usize field.

01Start with a look

A styled builder still creates an SDK button. Your application subscribes to the same SDK events.

Rust · Look → SDK entity
use gpui::Entity;
use xgpui_luma::controls::button::Button;
use xgpui_luma::infra::presenter::HasPresenter;
use xgpui_luma_look_shadcn as shadcn;

let save: Entity<Button> = shadcn::Button::new("save")
    .primary()
    .label("Save changes")
    .on_click(cx, |app, cx| {
        app.save_requests += 1;
        cx.notify();
    })
    .spawn(cx);

02Customize the content

Keep the look’s template. Replace the label with your own composition using .content(...).

Rust · Content presenter
use gpui::{div, prelude::*};
use xgpui_luma::infra::presenter::HasPresenter;
use xgpui_luma_look_shadcn as shadcn;

let save = shadcn::Button::new("save")
    .primary()
    .content(|model, _cx| {
        div().flex().gap_2()
            .text_color(model.look.foreground)
            .child("Save changes")
            .child("↗")
    })
    .spawn(cx);

The presenter receives resolved styling. The template keeps responsibility for the surrounding button.

03Override part of a template

Use .with_template_modifier(...) for a local adjustment. Both SDK and Look button builders support it.

Rust · Template modifier
use gpui::{px, prelude::*};
use xgpui_luma::infra::presenter::HasPresenter;
use xgpui_luma_look_shadcn as shadcn;

let save = shadcn::Button::new("save")
    .primary()
    .label("Save changes")
    .with_template_modifier(|root, _model| {
        root.min_w(px(160.0))
    })
    .spawn(cx);

This modifies the existing template’s output. The control still handles focus, activation, and events.

04Supply your own template

Implement ButtonTemplate when the structure needs to change. This example wraps the SDK’s default rendering to retain its state and focus visuals.

Rust · Template implementation
use gpui::{App, Div, Stateful, Window, div, prelude::*};
use xgpui_luma::controls::button::{
    ButtonRenderModel, ButtonTemplate, default_button_template,
};

struct WorkspaceButtonTemplate;

impl ButtonTemplate for WorkspaceButtonTemplate {
    fn render(
        &self,
        model: &ButtonRenderModel<()>,
        window: &mut Window,
        cx: &mut App,
    ) -> Stateful<Div> {
        let face = default_button_template().render(model, window, cx);

        div().id(model.id.clone())
            .flex().flex_col().gap_1()
            .child(face)
            .child(div().text_xs().child("Workspace action"))
    }
}
Rust · Install the template
use std::sync::Arc;
use xgpui_luma::controls::button::Button;
use xgpui_luma::infra::presenter::HasPresenter;

let save = Button::new("save")
    .label("Save changes")
    .template(Arc::new(WorkspaceButtonTemplate))
    .spawn(cx);

This uses the SDK’s stylesheet-backed baseline appearance. A completely custom template can render its own face instead; it must provide appropriate disabled and focus visuals. The SDK control retains its input and event wiring.

Where to look next

Template contract · SDK builder · Look adapter · Button guide