or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

client.mdcommands.mdcomponents.mddiscord-models.mdevents.mdextensions.mdindex.md

components.mddocs/

0

# User Interface Components

1

2

Interactive components like buttons, select menus, and modals for rich Discord bot interfaces.

3

4

## Action Rows

5

6

Action rows are containers that hold interactive components. Each message can have up to 5 action rows.

7

8

```python

9

from interactions import ActionRow, Button, ButtonStyle

10

11

# Create action row with button

12

button = Button(

13

style=ButtonStyle.PRIMARY,

14

label="Click Me!",

15

custom_id="my_button"

16

)

17

action_row = ActionRow(button)

18

19

# Send message with components

20

await ctx.send("Click the button below!", components=[action_row])

21

```

22

23

### Action Row Limits

24

25

```python

26

from interactions import ACTION_ROW_MAX_ITEMS

27

28

# Maximum components per action row

29

max_items = ACTION_ROW_MAX_ITEMS # 5

30

31

# Multiple components in one row

32

buttons = [

33

Button(style=ButtonStyle.PRIMARY, label=f"Button {i}", custom_id=f"btn_{i}")

34

for i in range(5)

35

]

36

action_row = ActionRow(*buttons)

37

```

38

39

## Buttons

40

41

### Basic Buttons

42

43

```python

44

from interactions import Button, ButtonStyle, ActionRow

45

46

@slash_command(name="buttons", description="Show different button types")

47

async def buttons_command(ctx: SlashContext):

48

# Different button styles

49

primary_btn = Button(

50

style=ButtonStyle.PRIMARY,

51

label="Primary",

52

custom_id="primary_button"

53

)

54

55

secondary_btn = Button(

56

style=ButtonStyle.SECONDARY,

57

label="Secondary",

58

custom_id="secondary_button"

59

)

60

61

success_btn = Button(

62

style=ButtonStyle.SUCCESS,

63

label="Success",

64

custom_id="success_button"

65

)

66

67

danger_btn = Button(

68

style=ButtonStyle.DANGER,

69

label="Danger",

70

custom_id="danger_button"

71

)

72

73

# Link button (doesn't send interaction)

74

link_btn = Button(

75

style=ButtonStyle.LINK,

76

label="Visit GitHub",

77

url="https://github.com"

78

)

79

80

row1 = ActionRow(primary_btn, secondary_btn, success_btn)

81

row2 = ActionRow(danger_btn, link_btn)

82

83

await ctx.send("Choose a button:", components=[row1, row2])

84

```

85

86

### Buttons with Emojis

87

88

```python

89

from interactions import Button, ButtonStyle, PartialEmoji

90

91

@slash_command(name="emoji_buttons", description="Buttons with emojis")

92

async def emoji_buttons(ctx: SlashContext):

93

# Unicode emoji

94

thumbs_up = Button(

95

style=ButtonStyle.SUCCESS,

96

label="Like",

97

emoji="πŸ‘",

98

custom_id="like_btn"

99

)

100

101

# Custom emoji (if bot has access)

102

custom_emoji = PartialEmoji(name="custom_emoji", id=12345678901234567)

103

custom_btn = Button(

104

style=ButtonStyle.PRIMARY,

105

emoji=custom_emoji,

106

custom_id="custom_btn"

107

)

108

109

# Emoji only (no label)

110

reaction_btn = Button(

111

style=ButtonStyle.SECONDARY,

112

emoji="❀️",

113

custom_id="heart_btn"

114

)

115

116

row = ActionRow(thumbs_up, custom_btn, reaction_btn)

117

await ctx.send("React with buttons!", components=[row])

118

```

119

120

### Disabled Buttons

121

122

```python

123

@slash_command(name="disabled", description="Show disabled buttons")

124

async def disabled_buttons(ctx: SlashContext):

125

enabled_btn = Button(

126

style=ButtonStyle.PRIMARY,

127

label="Enabled",

128

custom_id="enabled_btn"

129

)

130

131

disabled_btn = Button(

132

style=ButtonStyle.SECONDARY,

133

label="Disabled",

134

custom_id="disabled_btn",

135

disabled=True

136

)

137

138

row = ActionRow(enabled_btn, disabled_btn)

139

await ctx.send("One button is disabled:", components=[row])

140

```

141

142

### Button Callbacks

143

144

```python

145

from interactions import component_callback, ComponentContext

146

147

@component_callback("my_button")

148

async def button_callback(ctx: ComponentContext):

149

"""Handle button click"""

150

await ctx.send("Button clicked!", ephemeral=True)

151

152

@component_callback("like_btn")

153

async def like_button(ctx: ComponentContext):

154

"""Handle like button"""

155

user = ctx.author

156

await ctx.send(f"{user.mention} liked this!", ephemeral=True)

157

158

# Multiple button handler

159

@component_callback("btn_1", "btn_2", "btn_3")

160

async def multi_button_handler(ctx: ComponentContext):

161

"""Handle multiple buttons with one callback"""

162

button_id = ctx.custom_id

163

await ctx.send(f"You clicked {button_id}!", ephemeral=True)

164

```

165

166

## Select Menus

167

168

### String Select Menu

169

170

```python

171

from interactions import StringSelectMenu, StringSelectOption, ActionRow

172

173

@slash_command(name="select", description="Choose from options")

174

async def select_command(ctx: SlashContext):

175

options = [

176

StringSelectOption(

177

label="Option 1",

178

value="opt_1",

179

description="First option",

180

emoji="1️⃣"

181

),

182

StringSelectOption(

183

label="Option 2",

184

value="opt_2",

185

description="Second option",

186

emoji="2️⃣"

187

),

188

StringSelectOption(

189

label="Option 3",

190

value="opt_3",

191

description="Third option",

192

emoji="3️⃣",

193

default=True # Pre-selected

194

)

195

]

196

197

select_menu = StringSelectMenu(

198

*options,

199

placeholder="Choose an option...",

200

min_values=1,

201

max_values=2, # Allow selecting up to 2 options

202

custom_id="string_select"

203

)

204

205

row = ActionRow(select_menu)

206

await ctx.send("Make your selection:", components=[row])

207

208

@component_callback("string_select")

209

async def select_callback(ctx: ComponentContext):

210

"""Handle select menu interaction"""

211

selected_values = ctx.values # List of selected values

212

await ctx.send(f"You selected: {', '.join(selected_values)}", ephemeral=True)

213

```

214

215

### User Select Menu

216

217

```python

218

from interactions import UserSelectMenu

219

220

@slash_command(name="pick_user", description="Pick users")

221

async def pick_user_command(ctx: SlashContext):

222

user_select = UserSelectMenu(

223

placeholder="Select users...",

224

min_values=1,

225

max_values=5, # Allow selecting up to 5 users

226

custom_id="user_select"

227

)

228

229

row = ActionRow(user_select)

230

await ctx.send("Pick some users:", components=[row])

231

232

@component_callback("user_select")

233

async def user_select_callback(ctx: ComponentContext):

234

"""Handle user selection"""

235

# ctx.resolved contains the resolved users

236

selected_users = ctx.resolved.users if ctx.resolved else []

237

238

user_mentions = [user.mention for user in selected_users.values()]

239

await ctx.send(f"Selected users: {', '.join(user_mentions)}", ephemeral=True)

240

```

241

242

### Role Select Menu

243

244

```python

245

from interactions import RoleSelectMenu

246

247

@slash_command(name="pick_role", description="Pick roles")

248

async def pick_role_command(ctx: SlashContext):

249

role_select = RoleSelectMenu(

250

placeholder="Select roles...",

251

min_values=1,

252

max_values=3,

253

custom_id="role_select"

254

)

255

256

row = ActionRow(role_select)

257

await ctx.send("Pick some roles:", components=[row])

258

259

@component_callback("role_select")

260

async def role_select_callback(ctx: ComponentContext):

261

"""Handle role selection"""

262

selected_roles = ctx.resolved.roles if ctx.resolved else []

263

264

role_mentions = [role.mention for role in selected_roles.values()]

265

await ctx.send(f"Selected roles: {', '.join(role_mentions)}", ephemeral=True)

266

```

267

268

### Channel Select Menu

269

270

```python

271

from interactions import ChannelSelectMenu, ChannelType

272

273

@slash_command(name="pick_channel", description="Pick channels")

274

async def pick_channel_command(ctx: SlashContext):

275

channel_select = ChannelSelectMenu(

276

placeholder="Select channels...",

277

min_values=1,

278

max_values=2,

279

channel_types=[ChannelType.GUILD_TEXT, ChannelType.GUILD_VOICE], # Limit channel types

280

custom_id="channel_select"

281

)

282

283

row = ActionRow(channel_select)

284

await ctx.send("Pick some channels:", components=[row])

285

286

@component_callback("channel_select")

287

async def channel_select_callback(ctx: ComponentContext):

288

"""Handle channel selection"""

289

selected_channels = ctx.resolved.channels if ctx.resolved else []

290

291

channel_mentions = [f"#{channel.name}" for channel in selected_channels.values()]

292

await ctx.send(f"Selected channels: {', '.join(channel_mentions)}", ephemeral=True)

293

```

294

295

### Mentionable Select Menu

296

297

```python

298

from interactions import MentionableSelectMenu

299

300

@slash_command(name="pick_mentionable", description="Pick users or roles")

301

async def pick_mentionable_command(ctx: SlashContext):

302

mentionable_select = MentionableSelectMenu(

303

placeholder="Select users or roles...",

304

min_values=1,

305

max_values=5,

306

custom_id="mentionable_select"

307

)

308

309

row = ActionRow(mentionable_select)

310

await ctx.send("Pick users or roles:", components=[row])

311

312

@component_callback("mentionable_select")

313

async def mentionable_select_callback(ctx: ComponentContext):

314

"""Handle mentionable selection"""

315

mentions = []

316

317

if ctx.resolved:

318

# Add selected users

319

if ctx.resolved.users:

320

mentions.extend([user.mention for user in ctx.resolved.users.values()])

321

322

# Add selected roles

323

if ctx.resolved.roles:

324

mentions.extend([role.mention for role in ctx.resolved.roles.values()])

325

326

await ctx.send(f"Selected: {', '.join(mentions)}", ephemeral=True)

327

```

328

329

## Modals

330

331

### Basic Modal

332

333

```python

334

from interactions import Modal, InputText, TextStyles

335

336

@slash_command(name="feedback", description="Submit feedback")

337

async def feedback_command(ctx: SlashContext):

338

modal = Modal(

339

InputText(

340

label="Feedback Title",

341

placeholder="Enter a title for your feedback...",

342

custom_id="title",

343

style=TextStyles.SHORT,

344

required=True,

345

max_length=100

346

),

347

InputText(

348

label="Feedback Details",

349

placeholder="Describe your feedback in detail...",

350

custom_id="details",

351

style=TextStyles.PARAGRAPH,

352

required=True,

353

max_length=1000

354

),

355

InputText(

356

label="Contact Email",

357

placeholder="your.email@example.com",

358

custom_id="email",

359

style=TextStyles.SHORT,

360

required=False

361

),

362

title="Feedback Form",

363

custom_id="feedback_modal"

364

)

365

366

await ctx.send_modal(modal)

367

368

@modal_callback("feedback_modal")

369

async def feedback_modal_callback(ctx: ModalContext):

370

"""Handle feedback modal submission"""

371

title = ctx.responses["title"]

372

details = ctx.responses["details"]

373

email = ctx.responses["email"]

374

375

embed = Embed(

376

title="Feedback Received",

377

description=f"**Title:** {title}\n\n**Details:** {details}",

378

color=0x00ff00

379

)

380

381

if email:

382

embed.add_field(name="Contact", value=email, inline=False)

383

384

embed.set_footer(text=f"Submitted by {ctx.author}")

385

386

await ctx.send("Thank you for your feedback!", embed=embed, ephemeral=True)

387

```

388

389

### Pre-filled Modal

390

391

```python

392

@slash_command(name="edit_profile", description="Edit your profile")

393

async def edit_profile_command(ctx: SlashContext):

394

# Get user's current info (from database, etc.)

395

current_bio = get_user_bio(ctx.author.id)

396

current_status = get_user_status(ctx.author.id)

397

398

modal = Modal(

399

InputText(

400

label="Bio",

401

placeholder="Tell us about yourself...",

402

custom_id="bio",

403

style=TextStyles.PARAGRAPH,

404

value=current_bio, # Pre-fill with current value

405

max_length=500

406

),

407

InputText(

408

label="Status",

409

placeholder="What's your current status?",

410

custom_id="status",

411

style=TextStyles.SHORT,

412

value=current_status,

413

max_length=100

414

),

415

title="Edit Profile",

416

custom_id="profile_modal"

417

)

418

419

await ctx.send_modal(modal)

420

```

421

422

### Modal Input Types

423

424

```python

425

from interactions import ShortText, ParagraphText

426

427

# Alternative syntax using specific input classes

428

modal = Modal(

429

ShortText(

430

label="Username",

431

placeholder="Enter username...",

432

custom_id="username",

433

min_length=3,

434

max_length=20

435

),

436

ParagraphText(

437

label="Description",

438

placeholder="Describe yourself...",

439

custom_id="description",

440

min_length=10,

441

max_length=500

442

),

443

title="User Registration",

444

custom_id="register_modal"

445

)

446

```

447

448

## Advanced Component Patterns

449

450

### Dynamic Component Updates

451

452

```python

453

@component_callback("counter_btn")

454

async def counter_callback(ctx: ComponentContext):

455

"""Update button based on state"""

456

# Get current count (from message, database, etc.)

457

current_count = get_counter_value(ctx.message.id)

458

new_count = current_count + 1

459

460

# Update the button

461

updated_button = Button(

462

style=ButtonStyle.PRIMARY,

463

label=f"Count: {new_count}",

464

custom_id="counter_btn"

465

)

466

467

row = ActionRow(updated_button)

468

469

# Edit the message with updated components

470

await ctx.edit_origin(

471

content=f"Button clicked {new_count} times!",

472

components=[row]

473

)

474

475

# Update stored value

476

set_counter_value(ctx.message.id, new_count)

477

```

478

479

### Component State Management

480

481

```python

482

import json

483

484

@slash_command(name="poll", description="Create a poll")

485

@slash_option(name="question", description="Poll question", required=True, opt_type=OptionType.STRING)

486

async def create_poll(ctx: SlashContext, question: str):

487

"""Create a poll with voting buttons"""

488

489

# Create buttons with embedded state

490

yes_btn = Button(

491

style=ButtonStyle.SUCCESS,

492

label="Yes (0)",

493

custom_id="poll_yes",

494

emoji="βœ…"

495

)

496

497

no_btn = Button(

498

style=ButtonStyle.DANGER,

499

label="No (0)",

500

custom_id="poll_no",

501

emoji="❌"

502

)

503

504

row = ActionRow(yes_btn, no_btn)

505

506

embed = Embed(

507

title="πŸ“Š Poll",

508

description=question,

509

color=0x0099ff

510

)

511

512

message = await ctx.send(embed=embed, components=[row])

513

514

# Initialize poll data

515

init_poll_data(message.id, {"yes": 0, "no": 0, "voters": []})

516

517

@component_callback("poll_yes", "poll_no")

518

async def poll_vote_callback(ctx: ComponentContext):

519

"""Handle poll voting"""

520

vote_type = ctx.custom_id.split("_")[1] # "yes" or "no"

521

user_id = ctx.author.id

522

523

# Get current poll data

524

poll_data = get_poll_data(ctx.message.id)

525

526

# Check if user already voted

527

if user_id in poll_data["voters"]:

528

await ctx.send("You already voted in this poll!", ephemeral=True)

529

return

530

531

# Record vote

532

poll_data[vote_type] += 1

533

poll_data["voters"].append(user_id)

534

535

# Update buttons with new counts

536

yes_btn = Button(

537

style=ButtonStyle.SUCCESS,

538

label=f"Yes ({poll_data['yes']})",

539

custom_id="poll_yes",

540

emoji="βœ…"

541

)

542

543

no_btn = Button(

544

style=ButtonStyle.DANGER,

545

label=f"No ({poll_data['no']})",

546

custom_id="poll_no",

547

emoji="❌"

548

)

549

550

row = ActionRow(yes_btn, no_btn)

551

552

# Update message

553

await ctx.edit_origin(components=[row])

554

555

# Save poll data

556

save_poll_data(ctx.message.id, poll_data)

557

558

await ctx.send(f"Voted {vote_type.title()}!", ephemeral=True)

559

```

560

561

### Component Cleanup

562

563

```python

564

@component_callback("temp_button")

565

async def temp_button_callback(ctx: ComponentContext):

566

"""Button that removes itself after use"""

567

await ctx.send("Button used! Removing it now...", ephemeral=True)

568

569

# Remove components by editing message with empty components

570

await ctx.edit_origin(

571

content="Button was used and removed.",

572

components=[]

573

)

574

```

575

576

### Conditional Component Display

577

578

```python

579

@slash_command(name="admin_panel", description="Show admin panel")

580

async def admin_panel(ctx: SlashContext):

581

"""Show different components based on user permissions"""

582

583

components = []

584

585

# Basic buttons for everyone

586

info_btn = Button(

587

style=ButtonStyle.SECONDARY,

588

label="Server Info",

589

custom_id="server_info"

590

)

591

components.append(info_btn)

592

593

# Admin-only buttons

594

if ctx.author.guild_permissions.ADMINISTRATOR:

595

admin_btn = Button(

596

style=ButtonStyle.DANGER,

597

label="Admin Actions",

598

custom_id="admin_actions"

599

)

600

components.append(admin_btn)

601

602

# Moderator buttons

603

if ctx.author.guild_permissions.MANAGE_MESSAGES:

604

mod_btn = Button(

605

style=ButtonStyle.PRIMARY,

606

label="Mod Tools",

607

custom_id="mod_tools"

608

)

609

components.append(mod_btn)

610

611

# Organize into rows (max 5 per row)

612

rows = []

613

for i in range(0, len(components), 5):

614

row_components = components[i:i+5]

615

rows.append(ActionRow(*row_components))

616

617

await ctx.send("Control Panel:", components=rows)

618

```

619

620

## Component Utility Functions

621

622

### Spread Components to Rows

623

624

```python

625

from interactions import spread_to_rows

626

627

# Automatically organize components into action rows

628

buttons = [

629

Button(style=ButtonStyle.PRIMARY, label=f"Button {i}", custom_id=f"btn_{i}")

630

for i in range(8) # 8 buttons

631

]

632

633

# Spread across multiple rows (5 per row max)

634

rows = spread_to_rows(buttons, max_in_row=5)

635

# Results in 2 rows: first with 5 buttons, second with 3 buttons

636

637

await ctx.send("Multiple rows of buttons:", components=rows)

638

```

639

640

### Get Component IDs

641

642

```python

643

from interactions import get_components_ids

644

645

# Extract all custom IDs from components

646

components = [

647

ActionRow(

648

Button(style=ButtonStyle.PRIMARY, label="A", custom_id="btn_a"),

649

Button(style=ButtonStyle.SECONDARY, label="B", custom_id="btn_b")

650

),

651

ActionRow(

652

StringSelectMenu(

653

StringSelectOption(label="Option", value="val"),

654

custom_id="select_menu"

655

)

656

)

657

]

658

659

component_ids = get_components_ids(components)

660

# Returns: ["btn_a", "btn_b", "select_menu"]

661

```

662

663

## Component Limits & Constants

664

665

```python

666

from interactions import (

667

ACTION_ROW_MAX_ITEMS,

668

SELECTS_MAX_OPTIONS,

669

SELECT_MAX_NAME_LENGTH

670

)

671

672

# Component limits

673

ACTION_ROW_MAX_ITEMS # 5 - Max components per action row

674

SELECTS_MAX_OPTIONS # 25 - Max options in select menu

675

SELECT_MAX_NAME_LENGTH # 100 - Max length for select option names

676

```

677

678

## Error Handling

679

680

### Component Error Events

681

682

```python

683

@listen(events.ComponentError)

684

async def on_component_error(event: events.ComponentError):

685

"""Handle component interaction errors"""

686

ctx = event.ctx

687

error = event.error

688

689

print(f"Component error in {ctx.custom_id}: {error}")

690

691

# Try to send error message to user

692

try:

693

await ctx.send("An error occurred with this component.", ephemeral=True)

694

except:

695

pass # Interaction may have already been responded to

696

697

@listen(events.ModalError)

698

async def on_modal_error(event: events.ModalError):

699

"""Handle modal submission errors"""

700

ctx = event.ctx

701

error = event.error

702

703

print(f"Modal error in {ctx.custom_id}: {error}")

704

705

try:

706

await ctx.send("An error occurred processing your form.", ephemeral=True)

707

except:

708

pass

709

```

710

711

### Try-Catch in Callbacks

712

713

```python

714

@component_callback("risky_button")

715

async def risky_button_callback(ctx: ComponentContext):

716

"""Button callback with error handling"""

717

try:

718

# Risky operation that might fail

719

result = await perform_risky_operation(ctx.author.id)

720

await ctx.send(f"Operation successful: {result}", ephemeral=True)

721

722

except ValueError as e:

723

await ctx.send(f"Invalid input: {e}", ephemeral=True)

724

725

except PermissionError:

726

await ctx.send("You don't have permission for this action.", ephemeral=True)

727

728

except Exception as e:

729

await ctx.send("An unexpected error occurred.", ephemeral=True)

730

# Log the error

731

print(f"Unexpected error in risky_button: {e}")

732

```