Accordion

An accordion component that allows users to show and hide sections of content. It uses collapse functionality internally to create collapsible panels.

Install

#Import

use gpui_kit::component::accordion::Accordion;
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { AccordionEntry, AccordionItem, Accordion } from "accordion.slint";
use std::collections::BTreeSet;

#Usage

#Single

Accordion::new("my-accordion")
    .item(|item| {
        item.title("Section 1")
            .child("Content for section 1")
    })
    .item(|item| {
        item.title("Section 2")
            .child("Content for section 2")
    })
    .item(|item| {
        item.title("Section 3")
            .child("Content for section 3")
    })
Accordion {
    open-index: 0;
    items: [
        { title: "What is GPUI Kit?", content: "A Rust desktop application framework built on GPUI, with styled components inspired by macOS and Windows controls." },
        { title: "Which platforms are supported?", content: "macOS on Apple silicon and Intel, Linux on x86_64, and Windows on x86_64." },
        { title: "Can I restyle the components?", content: "Yes. Every component reads semantic theme tokens, so switching the palette restyles the whole application." },
    ];
    toggled(index, open) => {
        root.last-toggle = open ? "Section \{index + 1} opened." : "Section \{index + 1} closed.";
    }
}
Text {
    text: root.last-toggle;
    color: Theme.muted-foreground;
    font-size: Theme.font-size-xs;
}
// Opening one section closes the others: single mode is the caller's own
// rule over its open set, which is why the last of these three is not open.
let single = panel(
    app,
    SINGLE,
    true,
    ControlSize::Medium,
    true,
    &[
        (
            "What is Iced?",
            "A Rust desktop application framework built on iced, with styled \
             components inspired by macOS and Windows controls.",
        ),
        (
            "Which platforms are supported?",
            "macOS on Apple silicon and Intel, Linux on x86_64, and Windows on \
             x86_64.",
        ),
        (
            "Can I restyle the components?",
            "Yes. Every component reads semantic theme tokens, so switching the \
             palette restyles the whole application.",
        ),
    ],
);

#Multiple

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The open set holds every open section, so an accordion the caller does not
// close on a toggle simply allows several — two are open on boot.
let multiple = panel(
    app,
    MULTIPLE,
    false,
    ControlSize::Medium,
    true,
    &[
        (
            "Shipping",
            "Orders ship within two business days from our warehouse.",
        ),
        (
            "Returns",
            "Return any unused item within 30 days for a full refund.",
        ),
        (
            "Warranty",
            "Hardware is covered for two years against manufacturing defects.",
        ),
    ],
);

By default, only one accordion item can be open at a time. Use multiple() to allow multiple items to be open:

Accordion::new("my-accordion")
    .multiple(true)
    .item(|item| item.title("Section 1").child("Content 1"))
    .item(|item| item.title("Section 2").child("Content 2"))
Accordion {
    multiple: true;
    open-index: 0;
    items: [
        { title: "Shipping", content: "Orders ship within two business days from our warehouse." },
        { title: "Returns", content: "Return any unused item within 30 days for a full refund." },
        { title: "Warranty", content: "Hardware is covered for two years against manufacturing defects." },
    ];
}

#Icons and custom content

Accordion::new("my-accordion")
    .item(|item| {
        item.title(
            h_flex()
                .gap_2()
                .child(Icon::new(IconName::Settings))
                .child("Settings")
        )
        .child("Settings content here")
    })
Accordion {
    AccordionItem {
        title: "Notifications";
        icon: "bell";
        open: true;
        VerticalLayout {
            spacing: 8px;
            Text {
                text: root.notifications == 0 ? "You're all caught up." : "\{root.notifications} unread notifications.";
                color: Theme.foreground;
                font-size: Theme.font-size-sm;
            }
            GridLayout {
                spacing: 8px;
                Button {
                    horizontal-stretch: 0;
                    text: "Mark all as read";
                    size: ControlSize.small;
                    enabled: root.notifications > 0;
                    clicked => {
                        root.notifications = 0;
                    }
                }
                Button {
                    horizontal-stretch: 0;
                    text: "Simulate new";
                    size: ControlSize.small;
                    variant: ButtonVariant.ghost;
                    icon: "plus";
                    clicked => {
                        root.notifications += 1;
                    }
                }
            }
        }
    }
    AccordionItem {
        title: "Settings";
        icon: "settings";
        Text {
            text: "Preferences for appearance, keyboard shortcuts, and updates.";
            color: Theme.foreground;
            font-size: Theme.font-size-sm;
            wrap: word-wrap;
        }
    }
    AccordionItem {
        title: "Nested sections";
        icon: "layers";
        last: true;
        Accordion {
            size: ControlSize.small;
            items: [
                { title: "Child section 1", content: "Content of the first child section." },
                { title: "Child section 2", content: "Content of the second child section." },
            ];
        }
    }
}
// The header is the caller's element, so a leading icon is just another
// child; the panel is the caller's too, so the notification controls below
// live inside a section.
let unread = if app.notifications == 0 {
    "You're all caught up.".to_string()
} else {
    format!("{} unread notifications.", app.notifications)
};
let icons = Accordion::new([
    Section::new(labeled_header(
        mark(BELL, 14.0, muted),
        "Notifications",
        theme::FONT_SIZE_SM,
        8.0,
    ))
    .on_toggle(toggle(app, ICONS, 0, "Notifications", false))
    .open(is_open(app, ICONS, 0))
    .content(column![
        text(unread)
            .size(theme::FONT_SIZE_SM)
            .style(|theme: &Theme| text::Style {
                color: Some(Tokens::of(theme).foreground),
            }),
        row![
            Button::new("Mark all as read")
                .size(ControlSize::XSmall)
                .on_press(Message::MarkAllRead),
            Button::new("Simulate new")
                .variant(Variant::Ghost)
                .size(ControlSize::XSmall)
                .icon(mark(PLUS, 14.0, |theme| {
                    Tokens::of(theme).muted_foreground
                }))
                .on_press(Message::SimulateNew),
        ]
        .spacing(8.0),
    ]),
    Section::new(labeled_header(
        mark(SETTINGS, 14.0, muted),
        "Settings",
        theme::FONT_SIZE_SM,
        8.0,
    ))
    .on_toggle(toggle(app, ICONS, 1, "Settings", false))
    .open(is_open(app, ICONS, 1))
    .content(line(
        "Preferences for appearance, keyboard shortcuts, and updates.",
        theme::FONT_SIZE_SM,
        muted,
    )),
])
.size(ControlSize::Medium);

#Icons and custom content — 2 of 2

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The marker repeats the heading above because the page has no heading of
// its own for a caller-supplied disclosure: this example sits under the
// closest one the page documents. `Section::disclosure` hands the trailing
// lane the caller's element — here Lucide's arrow in place of the
// component's default chevron — so the icon set stays the caller's.
let custom_icon = Accordion::new([
    arrow_section(
        app,
        CUSTOM_ICON,
        0,
        "Custom arrow",
        "The disclosure in the trailing lane is the caller's arrow.",
    ),
    arrow_section(
        app,
        CUSTOM_ICON,
        1,
        "Second section",
        "Each section draws the mark its own open state wants.",
    ),
])
.size(ControlSize::Medium);

#Sizes

use gpui_kit::component::{Sizable as _, Size};

Accordion::new("my-accordion")
    .small()
    .item(|item| item.title("Small Section").child("Content"))

Accordion::new("my-accordion")
    .large()
    .item(|item| item.title("Large Section").child("Content"))
GridLayout {
    spacing: 16px;
    Accordion {
        horizontal-stretch: 1;
        size: ControlSize.xsmall;
        open-index: 0;
        items: [
            { title: "Extra small", content: "Compact rows for dense panels." },
            { title: "Second section", content: "Content." },
        ];
    }
    Accordion {
        horizontal-stretch: 1;
        size: ControlSize.small;
        open-index: 0;
        items: [
            { title: "Small", content: "Rows for sidebars and inspectors." },
            { title: "Second section", content: "Content." },
        ];
    }
    Accordion {
        horizontal-stretch: 1;
        size: ControlSize.large;
        open-index: 0;
        items: [
            { title: "Large", content: "Roomy rows for settings pages." },
            { title: "Second section", content: "Content." },
        ];
    }
}
// The size ladder: compact rows at xsmall, sidebar rows at small, roomy rows
// at large. iced has no wrapping grid, so the three stack.
let sizes = column![
    caption("Extra small"),
    panel(
        app,
        XSMALL,
        false,
        ControlSize::XSmall,
        true,
        &[
            ("Extra small", "Compact rows for dense panels."),
            ("Second section", "Content."),
        ],
    ),
    caption("Small"),
    panel(
        app,
        SMALL,
        false,
        ControlSize::Small,
        true,
        &[
            ("Small", "Rows for sidebars and inspectors."),
            ("Second section", "Content."),
        ],
    ),
    caption("Large"),
    panel(
        app,
        LARGE,
        false,
        ControlSize::Large,
        true,
        &[
            ("Large", "Roomy rows for settings pages."),
            ("Second section", "Content."),
        ],
    ),
]
.spacing(8.0);

#Borderless and disabled

Accordion::new("my-accordion")
    .disabled(true)
    .item(|item| item.title("Disabled Section").child("Content"))
GridLayout {
    spacing: 24px;
    Accordion {
        horizontal-stretch: 1;
        bordered: false;
        open-index: 0;
        items: [
            { title: "Borderless", content: "Items are still separated by a line." },
            { title: "Second section", content: "Content." },
        ];
    }
    Accordion {
        horizontal-stretch: 1;
        enabled: false;
        items: [
            { title: "Disabled section", content: "Content." },
            { title: "Another disabled section", content: "Content." },
        ];
    }
}
// Borderless drops the card but keeps the separating rule; disabled is the
// caller not wiring a header at all, drawn in the muted ink the component's
// `ink` helper gives a disabled title.
let borderless_and_disabled = column![
    panel(
        app,
        BORDERLESS,
        false,
        ControlSize::Medium,
        false,
        &[
            ("Borderless", "Items are still separated by a line."),
            ("Second section", "Content."),
        ],
    ),
    Accordion::new([
        Section::new(disabled_header(
            "Disabled section",
            label_size(ControlSize::Medium)
        ))
        .open(false)
        .content(line("Content.", theme::FONT_SIZE_SM, muted)),
        Section::new(disabled_header(
            "Another disabled section",
            label_size(ControlSize::Medium),
        )),
    ])
    .size(ControlSize::Medium),
]
.spacing(16.0);

#With Borders

Accordion::new("my-accordion")
    .bordered(true)
    .item(|item| item.title("Section 1").child("Content 1"))
SlintNo Slint version of this example yet.
// `bordered(true)` is the default and draws the single rounded card the
// sections sit in, with the padding that keeps the border off the rows.
let with_borders = panel(
    app,
    BORDERED,
    false,
    ControlSize::Medium,
    true,
    &[("Section 1", "Content 1"), ("Section 2", "Content 2")],
);

#Handle Toggle Events

Accordion::new("my-accordion")
    .on_toggle_click(|open_indices, window, cx| {
        println!("Open items: {:?}", open_indices);
    })
    .item(|item| item.title("Section 1").child("Content 1"))
SlintNo Slint version of this example yet.
// Every header reports the caller's message with the section's new state, so
// the caller writes the open set and can show what it received.
let events = panel(
    app,
    EVENTS,
    true,
    ControlSize::Medium,
    true,
    &[
        ("Section 1", "The first section."),
        ("Section 2", "The second section."),
        ("Section 3", "The third section."),
    ],
);
let events = column![
    events,
    line(
        app.notice
            .as_deref()
            .unwrap_or("Click a section title to toggle it."),
        theme::FONT_SIZE_XS,
        muted,
    ),
]
.spacing(8.0);

#Nested Accordions

Accordion::new("outer")
    .item(|item| {
        item.title("Parent Section")
            .child(
                Accordion::new("inner")
                    .item(|item| item.title("Child 1").child("Content"))
                    .item(|item| item.title("Child 2").child("Content"))
            )
    })
SlintNo Slint version of this example yet.
// A section's content is the caller's, so it can be another accordion; the
// inner one reveals inside the outer one's reveal.
let inner = Accordion::new([
    Section::new(header(
        "Child section 1",
        label_size(ControlSize::Small),
        |theme| Tokens::of(theme).foreground,
    ))
    .on_toggle(toggle(app, INNER, 0, "Child section 1", true))
    .open(is_open(app, INNER, 0))
    .content(line(
        "Content of the first child section.",
        theme::FONT_SIZE_SM,
        muted,
    )),
    Section::new(header(
        "Child section 2",
        label_size(ControlSize::Small),
        |theme| Tokens::of(theme).foreground,
    ))
    .on_toggle(toggle(app, INNER, 1, "Child section 2", true))
    .open(is_open(app, INNER, 1))
    .content(line(
        "Content of the second child section.",
        theme::FONT_SIZE_SM,
        muted,
    )),
])
.size(ControlSize::Small);

let nested = Accordion::new([Section::new(header(
    "Parent section",
    label_size(ControlSize::Medium),
    |theme| Tokens::of(theme).foreground,
))
.on_toggle(toggle(app, OUTER, 0, "Parent section", false))
.open(is_open(app, OUTER, 0))
.content(inner)])
.size(ControlSize::Medium);

#API Reference

#Sizing

Implements Sizable trait:

  • small() - Small size
  • medium() - Medium size (default)
  • large() - Large size
  • xsmall() - Extra small size

#AccordionEntry

One section of a data-driven `Accordion`.

NameKindTypeDefault
titlefieldstring
iconfieldstring
contentfieldstring

#AccordionItem

One collapsible section. Mirrors GPUI Component's `AccordionItem`: a title row with an optional leading icon that toggles a content panel, sizes, a disabled state, and an animated chevron and reveal. Put the content in `@children`, and set `last` on the final item to drop its separator.

NameKindTypeDefault
titleinstring
iconinstring
sizeinControlSizeControlSize.medium
enabledinbooltrue
lastinbool
openin-outbool
toggledcallback(open: bool)

#Accordion

A vertical stack of collapsible sections. Mirrors GPUI Component's `Accordion`: one open section at a time, or several with `multiple`, a bordered card by default, sizes, a disabled state, and a toggle event. Pass text sections through `items`, or compose `AccordionItem` children for rich content; `open-index` is the open section in single mode.

NameKindTypeDefault
itemsin[AccordionEntry]
multipleinbool
borderedinbooltrue
enabledinbooltrue
sizeinControlSizeControlSize.medium
open-indexin-outint-1
toggledcallback(index: int, open: bool)