Skip to content

Components

CascadeUI components extend discord.py's built-in UI components with automatic state dispatching and composability.

Stateful Components

These work in both V1 and V2 views.

StatefulButton

Extends discord.ui.Button with automatic COMPONENT_INTERACTION dispatching:

from cascadeui import StatefulButton

button = StatefulButton(
    label="Click Me",
    style=discord.ButtonStyle.primary,
    callback=my_handler,
)

Every click dispatches a COMPONENT_INTERACTION action to the state store, tracking the interaction for debugging and state history.

In V2 views, buttons must be wrapped in ActionRow:

from discord.ui import ActionRow

self.add_item(ActionRow(
    StatefulButton(label="Save", callback=self.save),
    StatefulButton(label="Cancel", callback=self.cancel),
))

StatefulSelect

Extends discord.ui.Select with the same state integration:

from cascadeui import StatefulSelect

select = StatefulSelect(
    placeholder="Pick one...",
    options=[
        discord.SelectOption(label="Option A", value="a"),
        discord.SelectOption(label="Option B", value="b"),
    ],
    callback=my_handler,
)

Specialized select variants are also available: Dropdown, RoleSelect, ChannelSelect, UserSelect, MentionableSelect.


V2 Helpers

Convenience functions for building V2 component trees. These return standard discord.py V2 components — no custom classes needed.

card()

Creates a Container with an optional title, children, and accent color:

from cascadeui import card, divider
from discord.ui import TextDisplay

self.add_item(
    card(
        "## My Card Title",
        TextDisplay("Card content goes here."),
        divider(),
        TextDisplay("-# Footer text"),
        color=discord.Color.blurple(),
    )
)

The first string argument becomes a TextDisplay heading. All subsequent arguments are added as children. color sets the container's accent color (like embed color, but stackable — multiple cards can have different colors in one message).

key_value()

Converts a dict to a formatted TextDisplay:

from cascadeui import key_value

self.add_item(key_value({
    "Status": "Online",
    "Users": "42",
    "Uptime": "3h 12m",
}))

Renders as:

**Status:** Online
**Users:** 42
**Uptime:** 3h 12m

action_section()

Creates a Section with text and a button accessory (text + action on the same line):

from cascadeui import action_section

self.add_item(
    action_section(
        "Click to refresh the dashboard",
        label="Refresh",
        callback=self.refresh,
        emoji="\U0001f504",
    )
)

toggle_section()

Creates a Section with a green/red toggle button:

from cascadeui import toggle_section

self.add_item(
    toggle_section(
        "**Dark Mode**\nEnable dark theme",
        active=self.dark_mode,
        callback=self.toggle_dark,
    )
)

When active=True, the button shows a green checkmark. When False, a red X.

alert()

A colored status container for success, warning, error, or info messages:

from cascadeui import alert

self.add_item(alert("Settings saved successfully!", level="success"))
self.add_item(alert("No data found.", level="info"))
Level Color
"success" Green
"warning" Gold
"error" Red
"info" Blue

divider() and gap()

Visual separators inside containers:

from cascadeui import divider, gap

# Thin line separator
self.add_item(divider())

# Larger spacing between content blocks
self.add_item(gap())

# Large spacing variant
self.add_item(gap(large=True))

divider() is an alias for Separator(spacing=SeparatorSpacing.small). gap() creates spacing without a visible line.

image_section()

A Section with a Thumbnail image:

from cascadeui import image_section

self.add_item(
    image_section("User avatar", url="https://example.com/avatar.png")
)

A MediaGallery from a list of image URLs:

from cascadeui import gallery

self.add_item(gallery(["https://example.com/img1.png", "https://example.com/img2.png"]))

Utilities

slugify()

Converts display strings to safe custom_id fragments:

from cascadeui import slugify

slugify("Color Roles")    # "color-roles"
slugify("He/Him")         # "hehim"

Useful for building stable custom_id values in persistent views where the ID must survive restarts:

custom_id=f"roles:{slugify(category)}:{slugify(role_name)}"

Modals

CascadeUI's Modal wraps discord.ui.Modal with state integration and optional validation.

Opening a Modal

Open a modal from any button callback using interaction.response.send_modal():

from cascadeui import Modal, TextInput

async def open_feedback(interaction):
    modal = Modal(
        title="Send Feedback",
        inputs=[
            TextInput(label="Subject", placeholder="Brief summary"),
            TextInput(label="Details", style=discord.TextStyle.long),
        ],
        callback=handle_feedback,
    )
    await interaction.response.send_modal(modal)

async def handle_feedback(interaction, values):
    subject = values.get("input_subject")
    details = values.get("input_details")
    await interaction.response.send_message(
        f"Thanks! Received: {subject}", ephemeral=True
    )

TextInput generates a custom_id from the label automatically (e.g. "Subject" becomes "input_subject"). You can also pass raw discord.ui.TextInput items if you need full control over the custom_id.

Validation

Pass a validators dict mapping custom_id to a list of validator functions:

from cascadeui import Modal, TextInput, min_length, max_length, regex

modal = Modal(
    title="Create Tag",
    inputs=[
        TextInput(label="Name", placeholder="tag-name"),
        TextInput(label="Content", style=discord.TextStyle.long),
    ],
    callback=save_tag,
    validators={
        "input_name": [
            min_length(2, "Tag name must be at least 2 characters"),
            max_length(32, "Tag name must be at most 32 characters"),
            regex(r"^[a-z0-9-]+$", "Only lowercase letters, numbers, and hyphens"),
        ],
        "input_content": [
            min_length(1, "Content cannot be empty"),
        ],
    },
)

All validators from the validation system work here.

State Integration

Pass view_id to dispatch a MODAL_SUBMITTED action to the state store:

modal = Modal(
    title="Edit Name",
    inputs=[TextInput(label="Name")],
    callback=handle_edit,
    view_id=self.id,
)

Component Wrappers

Wrappers modify component behavior without changing the component itself. Apply them to buttons to add cross-cutting behavior.

Wrappers consume the interaction response

All three wrappers (with_loading_state, with_confirmation, with_cooldown) use interaction.response internally to show their UI. Your wrapped callback must use interaction.followup.send() instead of interaction.response.send_message().

Loading State

from cascadeui import with_loading_state

button = StatefulButton(label="Process", callback=my_handler)
with_loading_state(button)

The button is disabled and its label changes to "Loading..." while the callback runs.

Confirmation Prompt

from cascadeui import with_confirmation

button = StatefulButton(label="Delete", callback=handle_delete)
with_confirmation(
    button,
    message="Are you sure you want to delete this?",
    confirmed_message="Deleted.",
    cancelled_message="Kept safe.",
)

Per-User Cooldown

from cascadeui import with_cooldown

button = StatefulButton(label="Claim", callback=handle_claim)
with_cooldown(button, seconds=10)
Scope Behavior
"user" (default) Each user has an independent cooldown
"guild" Shared cooldown per server
"global" One cooldown for everyone

V1 Composite Components

V1 only

These components extend CompositeComponent and use row-based layout. They work with StatefulView but are not compatible with StatefulLayoutView (V2). For V2, use the helper functions above or build component trees directly.

ConfirmationButtons

from cascadeui import ConfirmationButtons

confirmation = ConfirmationButtons(on_confirm=handle_confirm, on_cancel=handle_cancel)
confirmation.add_to_view(my_view)

PaginationControls

from cascadeui import PaginationControls

pagination = PaginationControls(page_count=5, on_page_change=handle_page)
pagination.add_to_view(my_view)

FormLayout

from cascadeui import FormLayout

layout = FormLayout(fields=[
    {"id": "color", "type": "select", "label": "Color",
     "options": ["Red", "Blue", "Green"]},
    {"id": "enabled", "type": "boolean", "label": "Enabled"},
])
layout.add_to_view(my_view)

ToggleGroup

from cascadeui import ToggleGroup

group = ToggleGroup(options=["Easy", "Medium", "Hard"], on_select=handler, default="Medium")
group.add_to_view(my_view)

ProgressBar

A text-based progress bar for embed fields (not a Discord component):

from cascadeui import ProgressBar

bar = ProgressBar(total=100, width=20)
embed.add_field(name="Progress", value=bar.render(65))
# Output: ████████████░░░░░░░░ 65%