or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

app-commands.mdcommands-framework.mdcore-objects.mdevent-handling.mdindex.mduser-interface.mdutilities.mdvoice-audio.mdwebhooks.md

user-interface.mddocs/

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

```