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
```