0
# User Interface Components
1
2
Interactive UI elements that enable rich user interactions beyond traditional text commands. Discord.py provides buttons, select menus, modals, and views for creating sophisticated bot interfaces directly integrated with Discord's native UI.
3
4
## Capabilities
5
6
### Views & Component Containers
7
8
Views manage collections of interactive components and handle their lifecycle.
9
10
```python { .api }
11
class View:
12
"""
13
Container for UI components with interaction handling.
14
15
Parameters:
16
- timeout: Seconds before view times out (None for no timeout)
17
"""
18
def __init__(self, *, timeout: Optional[float] = 180.0): ...
19
20
timeout: Optional[float] # Timeout in seconds
21
children: List[Item] # Child components
22
23
def add_item(self, item: Item) -> View:
24
"""Add a component to the view."""
25
26
def remove_item(self, item: Item) -> View:
27
"""Remove a component from the view."""
28
29
def clear_items(self) -> View:
30
"""Remove all components from the view."""
31
32
def get_item(self, custom_id: str) -> Optional[Item]:
33
"""Get component by custom_id."""
34
35
@property
36
def is_finished(self) -> bool:
37
"""Whether the view has finished (timed out or stopped)."""
38
39
@property
40
def is_persistent(self) -> bool:
41
"""Whether the view is persistent (no timeout)."""
42
43
def stop(self) -> None:
44
"""Stop the view and disable all components."""
45
46
async def wait(self) -> bool:
47
"""Wait for the view to finish (returns True if not timed out)."""
48
49
# Lifecycle hooks
50
async def on_timeout(self) -> None:
51
"""Called when view times out."""
52
53
async def on_error(self, interaction: Interaction, error: Exception, item: Item) -> None:
54
"""Called when component interaction raises an exception."""
55
56
# Interaction check
57
async def interaction_check(self, interaction: Interaction) -> bool:
58
"""Check if interaction should be processed."""
59
return True
60
61
class Modal:
62
"""
63
Modal dialog for collecting user input.
64
65
Parameters:
66
- title: Modal title displayed to user
67
- timeout: Seconds before modal times out
68
- custom_id: Custom ID for the modal
69
"""
70
def __init__(
71
self,
72
*,
73
title: str,
74
timeout: Optional[float] = None,
75
custom_id: str = None
76
): ...
77
78
title: str # Modal title
79
timeout: Optional[float] # Timeout in seconds
80
custom_id: str # Custom ID
81
children: List[TextInput] # Text input components
82
83
def add_item(self, item: TextInput) -> Modal:
84
"""Add a text input to the modal."""
85
86
def remove_item(self, item: TextInput) -> Modal:
87
"""Remove a text input from the modal."""
88
89
def clear_items(self) -> Modal:
90
"""Remove all text inputs from the modal."""
91
92
def stop(self) -> None:
93
"""Stop the modal."""
94
95
async def wait(self) -> bool:
96
"""Wait for the modal to be submitted."""
97
98
# Lifecycle hooks
99
async def on_submit(self, interaction: Interaction) -> None:
100
"""Called when modal is submitted."""
101
102
async def on_timeout(self) -> None:
103
"""Called when modal times out."""
104
105
async def on_error(self, interaction: Interaction, error: Exception) -> None:
106
"""Called when modal interaction raises an exception."""
107
108
# Interaction check
109
async def interaction_check(self, interaction: Interaction) -> bool:
110
"""Check if interaction should be processed."""
111
return True
112
```
113
114
### Buttons
115
116
Interactive buttons with various styles and emoji support.
117
118
```python { .api }
119
class Button(ui.Item):
120
"""
121
Interactive button component.
122
123
Parameters:
124
- style: Button style (primary, secondary, success, danger, link)
125
- label: Button text label
126
- disabled: Whether button is disabled
127
- custom_id: Custom ID for the button
128
- url: URL for link-style buttons
129
- emoji: Button emoji
130
- row: Row number (0-4) for button placement
131
"""
132
def __init__(
133
self,
134
*,
135
style: ButtonStyle = ButtonStyle.secondary,
136
label: Optional[str] = None,
137
disabled: bool = False,
138
custom_id: Optional[str] = None,
139
url: Optional[str] = None,
140
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
141
row: Optional[int] = None
142
): ...
143
144
style: ButtonStyle # Button style
145
label: Optional[str] # Button label
146
disabled: bool # Whether button is disabled
147
custom_id: Optional[str] # Custom ID
148
url: Optional[str] # URL for link buttons
149
emoji: Optional[Union[str, Emoji, PartialEmoji]] # Button emoji
150
151
async def callback(self, interaction: Interaction) -> None:
152
"""Called when button is clicked."""
153
pass
154
155
class ButtonStyle(Enum):
156
"""Button style enumeration."""
157
primary = 1 # Blurple button
158
secondary = 2 # Grey button
159
success = 3 # Green button
160
danger = 4 # Red button
161
link = 5 # Link button (requires URL)
162
163
# Aliases
164
blurple = 1
165
grey = 2
166
gray = 2
167
green = 3
168
red = 4
169
url = 5
170
171
def button(
172
*,
173
label: Optional[str] = None,
174
custom_id: Optional[str] = None,
175
disabled: bool = False,
176
style: ButtonStyle = ButtonStyle.secondary,
177
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
178
row: Optional[int] = None
179
) -> Callable:
180
"""Decorator to create a button in a View."""
181
```
182
183
### Select Menus
184
185
Dropdown menus for choosing from predefined options or Discord entities.
186
187
```python { .api }
188
class Select(ui.Item):
189
"""
190
Base select menu component.
191
192
Parameters:
193
- custom_id: Custom ID for the select menu
194
- placeholder: Placeholder text when nothing is selected
195
- min_values: Minimum number of selections required
196
- max_values: Maximum number of selections allowed
197
- disabled: Whether select menu is disabled
198
- row: Row number (0-4) for menu placement
199
"""
200
def __init__(
201
self,
202
*,
203
custom_id: Optional[str] = None,
204
placeholder: Optional[str] = None,
205
min_values: int = 1,
206
max_values: int = 1,
207
disabled: bool = False,
208
row: Optional[int] = None
209
): ...
210
211
custom_id: Optional[str] # Custom ID
212
placeholder: Optional[str] # Placeholder text
213
min_values: int # Minimum selections
214
max_values: int # Maximum selections
215
disabled: bool # Whether menu is disabled
216
options: List[SelectOption] # Menu options (for string select)
217
values: List[Any] # Current selected values
218
219
async def callback(self, interaction: Interaction) -> None:
220
"""Called when selection is made."""
221
pass
222
223
class UserSelect(Select):
224
"""
225
Select menu for choosing users/members.
226
227
Values will be User or Member objects.
228
"""
229
def __init__(self, **kwargs): ...
230
231
values: List[Union[User, Member]] # Selected users
232
233
class RoleSelect(Select):
234
"""
235
Select menu for choosing roles.
236
237
Values will be Role objects.
238
"""
239
def __init__(self, **kwargs): ...
240
241
values: List[Role] # Selected roles
242
243
class MentionableSelect(Select):
244
"""
245
Select menu for choosing users or roles.
246
247
Values will be User, Member, or Role objects.
248
"""
249
def __init__(self, **kwargs): ...
250
251
values: List[Union[User, Member, Role]] # Selected mentionables
252
253
class ChannelSelect(Select):
254
"""
255
Select menu for choosing channels.
256
257
Parameters:
258
- channel_types: List of allowed channel types
259
260
Values will be GuildChannel objects.
261
"""
262
def __init__(self, *, channel_types: List[ChannelType] = None, **kwargs): ...
263
264
channel_types: List[ChannelType] # Allowed channel types
265
values: List[GuildChannel] # Selected channels
266
267
class SelectOption:
268
"""
269
Option for string select menus.
270
271
Parameters:
272
- label: Option display text
273
- value: Option value (returned when selected)
274
- description: Optional description text
275
- emoji: Optional emoji
276
- default: Whether option is selected by default
277
"""
278
def __init__(
279
self,
280
*,
281
label: str,
282
value: str,
283
description: Optional[str] = None,
284
emoji: Optional[Union[str, Emoji, PartialEmoji]] = None,
285
default: bool = False
286
): ...
287
288
label: str # Display text
289
value: str # Option value
290
description: Optional[str] # Description
291
emoji: Optional[Union[str, Emoji, PartialEmoji]] # Emoji
292
default: bool # Whether selected by default
293
294
def select(
295
*,
296
cls: Type[Select] = Select,
297
placeholder: Optional[str] = None,
298
custom_id: Optional[str] = None,
299
min_values: int = 1,
300
max_values: int = 1,
301
options: List[SelectOption] = None,
302
disabled: bool = False,
303
row: Optional[int] = None
304
) -> Callable:
305
"""Decorator to create a select menu in a View."""
306
```
307
308
### Text Input
309
310
Text input fields for modals to collect user text input.
311
312
```python { .api }
313
class TextInput(ui.Item):
314
"""
315
Text input field for modals.
316
317
Parameters:
318
- label: Input field label
319
- style: Input style (short or paragraph)
320
- custom_id: Custom ID for the input
321
- placeholder: Placeholder text
322
- default: Default input value
323
- required: Whether input is required
324
- min_length: Minimum text length
325
- max_length: Maximum text length
326
- row: Row number (0-4) for input placement
327
"""
328
def __init__(
329
self,
330
*,
331
label: str,
332
style: TextStyle = TextStyle.short,
333
custom_id: Optional[str] = None,
334
placeholder: Optional[str] = None,
335
default: Optional[str] = None,
336
required: bool = True,
337
min_length: Optional[int] = None,
338
max_length: Optional[int] = None,
339
row: Optional[int] = None
340
): ...
341
342
label: str # Input label
343
style: TextStyle # Input style
344
custom_id: Optional[str] # Custom ID
345
placeholder: Optional[str] # Placeholder text
346
default: Optional[str] # Default value
347
required: bool # Whether required
348
min_length: Optional[int] # Minimum length
349
max_length: Optional[int] # Maximum length
350
value: Optional[str] # Current input value
351
352
class TextStyle(Enum):
353
"""Text input style enumeration."""
354
short = 1 # Single line input
355
paragraph = 2 # Multi-line input
356
357
# Aliases
358
long = 2
359
```
360
361
### Base Item Class
362
363
Base class for all UI components with common functionality.
364
365
```python { .api }
366
class Item:
367
"""
368
Base class for all UI components.
369
"""
370
def __init__(self): ...
371
372
type: ComponentType # Component type
373
custom_id: Optional[str] # Custom ID
374
disabled: bool # Whether component is disabled
375
row: Optional[int] # Row placement
376
width: int # Component width (for layout)
377
view: Optional[View] # Parent view
378
379
@property
380
def is_dispatchable(self) -> bool:
381
"""Whether component can receive interactions."""
382
383
@property
384
def is_persistent(self) -> bool:
385
"""Whether component is persistent across bot restarts."""
386
387
def to_component_dict(self) -> Dict[str, Any]:
388
"""Convert component to Discord API format."""
389
390
def is_row_full(self, components: List[Item]) -> bool:
391
"""Check if adding this component would fill the row."""
392
393
def refresh_component(self, component: Component) -> None:
394
"""Refresh component from Discord data."""
395
```
396
397
## Usage Examples
398
399
### Basic View with Buttons
400
401
```python
402
import discord
403
from discord.ext import commands
404
405
class ConfirmView(discord.ui.View):
406
def __init__(self):
407
super().__init__(timeout=60.0)
408
self.value = None
409
410
@discord.ui.button(label='Confirm', style=discord.ButtonStyle.green)
411
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
412
await interaction.response.send_message('Confirmed!', ephemeral=True)
413
self.value = True
414
self.stop()
415
416
@discord.ui.button(label='Cancel', style=discord.ButtonStyle.red)
417
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
418
await interaction.response.send_message('Cancelled!', ephemeral=True)
419
self.value = False
420
self.stop()
421
422
@bot.command()
423
async def confirm_action(ctx):
424
view = ConfirmView()
425
message = await ctx.send('Do you want to proceed?', view=view)
426
427
# Wait for user interaction
428
await view.wait()
429
430
if view.value is None:
431
await message.edit(content='Timed out.', view=None)
432
elif view.value:
433
await message.edit(content='Action confirmed!', view=None)
434
else:
435
await message.edit(content='Action cancelled.', view=None)
436
```
437
438
### Select Menu Example
439
440
```python
441
class RoleSelectView(discord.ui.View):
442
def __init__(self, roles):
443
super().__init__()
444
self.roles = roles
445
446
# Add select menu with role options
447
options = [
448
discord.SelectOption(
449
label=role.name,
450
description=f"Select the {role.name} role",
451
value=str(role.id),
452
emoji="๐ญ"
453
)
454
for role in roles[:25] # Discord limits to 25 options
455
]
456
457
select = discord.ui.Select(
458
placeholder="Choose your role...",
459
options=options,
460
custom_id="role_select"
461
)
462
select.callback = self.role_callback
463
self.add_item(select)
464
465
async def role_callback(self, interaction: discord.Interaction):
466
role_id = int(interaction.data['values'][0])
467
role = discord.utils.get(self.roles, id=role_id)
468
469
if role in interaction.user.roles:
470
await interaction.user.remove_roles(role)
471
await interaction.response.send_message(f'Removed {role.name} role!', ephemeral=True)
472
else:
473
await interaction.user.add_roles(role)
474
await interaction.response.send_message(f'Added {role.name} role!', ephemeral=True)
475
476
@bot.command()
477
async def role_menu(ctx):
478
# Get some roles (filter as needed)
479
roles = [role for role in ctx.guild.roles if not role.is_default() and not role.managed]
480
481
if not roles:
482
await ctx.send('No assignable roles found.')
483
return
484
485
view = RoleSelectView(roles)
486
await ctx.send('Select a role to add/remove:', view=view)
487
```
488
489
### Modal Dialog Example
490
491
```python
492
class FeedbackModal(discord.ui.Modal, title='Feedback Form'):
493
def __init__(self):
494
super().__init__()
495
496
# Short text input
497
name = discord.ui.TextInput(
498
label='Name',
499
placeholder='Your name here...',
500
required=False,
501
max_length=50
502
)
503
504
# Long text input
505
feedback = discord.ui.TextInput(
506
label='Feedback',
507
style=discord.TextStyle.paragraph,
508
placeholder='Tell us what you think...',
509
required=True,
510
max_length=1000
511
)
512
513
# Rating input
514
rating = discord.ui.TextInput(
515
label='Rating (1-5)',
516
placeholder='5',
517
required=True,
518
min_length=1,
519
max_length=1
520
)
521
522
async def on_submit(self, interaction: discord.Interaction):
523
# Validate rating
524
try:
525
rating_num = int(self.rating.value)
526
if not 1 <= rating_num <= 5:
527
raise ValueError
528
except ValueError:
529
await interaction.response.send_message('Rating must be a number from 1 to 5!', ephemeral=True)
530
return
531
532
# Process feedback
533
embed = discord.Embed(title='Feedback Received', color=0x00ff00)
534
embed.add_field(name='Name', value=self.name.value or 'Anonymous', inline=False)
535
embed.add_field(name='Rating', value='โญ' * rating_num, inline=False)
536
embed.add_field(name='Feedback', value=self.feedback.value, inline=False)
537
embed.set_footer(text=f'From: {interaction.user}')
538
539
# Send to feedback channel (replace with actual channel ID)
540
feedback_channel = bot.get_channel(123456789)
541
if feedback_channel:
542
await feedback_channel.send(embed=embed)
543
544
await interaction.response.send_message('Thank you for your feedback!', ephemeral=True)
545
546
async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
547
await interaction.response.send_message('An error occurred. Please try again.', ephemeral=True)
548
549
class FeedbackView(discord.ui.View):
550
def __init__(self):
551
super().__init__(timeout=None) # Persistent view
552
553
@discord.ui.button(label='Give Feedback', style=discord.ButtonStyle.primary, emoji='๐')
554
async def feedback_button(self, interaction: discord.Interaction, button: discord.ui.Button):
555
modal = FeedbackModal()
556
await interaction.response.send_modal(modal)
557
558
@bot.command()
559
async def feedback(ctx):
560
view = FeedbackView()
561
embed = discord.Embed(
562
title='Feedback System',
563
description='Click the button below to give us your feedback!',
564
color=0x0099ff
565
)
566
await ctx.send(embed=embed, view=view)
567
```
568
569
### Advanced Multi-Component View
570
571
```python
572
class MusicControlView(discord.ui.View):
573
def __init__(self, voice_client):
574
super().__init__(timeout=300.0)
575
self.voice_client = voice_client
576
self.volume = 50
577
578
@discord.ui.button(emoji='โฏ๏ธ', style=discord.ButtonStyle.primary)
579
async def play_pause(self, interaction: discord.Interaction, button: discord.ui.Button):
580
if self.voice_client.is_playing():
581
self.voice_client.pause()
582
await interaction.response.send_message('โธ๏ธ Paused', ephemeral=True)
583
elif self.voice_client.is_paused():
584
self.voice_client.resume()
585
await interaction.response.send_message('โถ๏ธ Resumed', ephemeral=True)
586
else:
587
await interaction.response.send_message('Nothing to play', ephemeral=True)
588
589
@discord.ui.button(emoji='โน๏ธ', style=discord.ButtonStyle.secondary)
590
async def stop(self, interaction: discord.Interaction, button: discord.ui.Button):
591
self.voice_client.stop()
592
await interaction.response.send_message('โน๏ธ Stopped', ephemeral=True)
593
594
@discord.ui.button(emoji='โญ๏ธ', style=discord.ButtonStyle.secondary)
595
async def skip(self, interaction: discord.Interaction, button: discord.ui.Button):
596
self.voice_client.stop() # This will trigger the next song
597
await interaction.response.send_message('โญ๏ธ Skipped', ephemeral=True)
598
599
@discord.ui.select(
600
placeholder="Adjust volume...",
601
options=[
602
discord.SelectOption(label="๐ Low (25%)", value="25"),
603
discord.SelectOption(label="๐ Medium (50%)", value="50", default=True),
604
discord.SelectOption(label="๐ High (75%)", value="75"),
605
discord.SelectOption(label="๐ข Max (100%)", value="100"),
606
]
607
)
608
async def volume_select(self, interaction: discord.Interaction, select: discord.ui.Select):
609
self.volume = int(select.values[0])
610
# Apply volume change to audio source if supported
611
await interaction.response.send_message(f'๐ Volume set to {self.volume}%', ephemeral=True)
612
613
async def on_timeout(self):
614
# Disable all components when view times out
615
for item in self.children:
616
item.disabled = True
617
618
# Update the message to show timeout
619
try:
620
await self.message.edit(view=self)
621
except:
622
pass
623
624
@bot.command()
625
async def music_controls(ctx):
626
if not ctx.voice_client:
627
await ctx.send('Not connected to a voice channel.')
628
return
629
630
view = MusicControlView(ctx.voice_client)
631
embed = discord.Embed(title='๐ต Music Controls', color=0x9932cc)
632
message = await ctx.send(embed=embed, view=view)
633
view.message = message # Store message reference for timeout handling
634
```
635
636
### Persistent View (Survives Bot Restart)
637
638
```python
639
class PersistentRoleView(discord.ui.View):
640
def __init__(self):
641
super().__init__(timeout=None)
642
643
@discord.ui.button(
644
label='Get Updates Role',
645
style=discord.ButtonStyle.primary,
646
custom_id='persistent_view:updates_role' # Must be unique and persistent
647
)
648
async def updates_role(self, interaction: discord.Interaction, button: discord.ui.Button):
649
role = discord.utils.get(interaction.guild.roles, name='Updates')
650
651
if not role:
652
await interaction.response.send_message('Updates role not found!', ephemeral=True)
653
return
654
655
if role in interaction.user.roles:
656
await interaction.user.remove_roles(role)
657
await interaction.response.send_message('Removed Updates role!', ephemeral=True)
658
else:
659
await interaction.user.add_roles(role)
660
await interaction.response.send_message('Added Updates role!', ephemeral=True)
661
662
@bot.event
663
async def on_ready():
664
# Re-add persistent views on bot startup
665
bot.add_view(PersistentRoleView())
666
print(f'{bot.user} is ready!')
667
668
@bot.command()
669
async def setup_roles(ctx):
670
view = PersistentRoleView()
671
embed = discord.Embed(
672
title='Role Assignment',
673
description='Click the button to toggle the Updates role.',
674
color=0x00ff00
675
)
676
await ctx.send(embed=embed, view=view)
677
```