port-from-bslib by posit-dev
Comprehensive guide for porting UI components from R's bslib package to py-shiny. Use this skill when: (1) User asks to "port this feature" or "port a component" and mentions bslib or links to a bslib PR (e.g., github.com/rstudio/bslib/pull/...), (2) Porting a new component from bslib to py-shiny, (3) Adding a new input, output, or UI component that exists in bslib, (4) Implementing feature parity with bslib, (5) Working on bslib-related features or components. Covers the complete workflow including understanding source implementation, creating Python equivalents, vendoring assets (SCSS, CSS, JavaScript), creating tests, documentation, and all necessary project files.
Why Use This
This skill provides specialized capabilities for posit-dev's codebase.
Use Cases
- Developing new features in the posit-dev repository
- Refactoring existing code to follow posit-dev standards
- Understanding and working with posit-dev's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Skill Stats
---
name: port-from-bslib
description: Comprehensive guide for porting UI components from R's bslib package to py-shiny. Use this skill when: (1) User asks to "port this feature" or "port a component" and mentions bslib or links to a bslib PR (e.g., github.com/rstudio/bslib/pull/...), (2) Porting a new component from bslib to py-shiny, (3) Adding a new input, output, or UI component that exists in bslib, (4) Implementing feature parity with bslib, (5) Working on bslib-related features or components. Covers the complete workflow including understanding source implementation, creating Python equivalents, vendoring assets (SCSS, CSS, JavaScript), creating tests, documentation, and all necessary project files.
---
# Porting Components from bslib to py-shiny
This guide explains how to port new UI components from the R bslib package to py-shiny. It assumes you're an experienced developer familiar with Python, R, and JavaScript/TypeScript, but may be new to the specifics of these repositories.
## Background
The bslib R package serves as the primary development location for new Bootstrap 5 components in the Shiny ecosystem. Components developed in bslib are then ported to py-shiny to maintain feature parity between Shiny for R and Shiny for Python.
**Key relationship**: bslib provides both the R implementation and the compiled JavaScript/CSS assets that py-shiny vendors and uses directly.
## Overview of the Porting Process
The porting process involves three main phases:
1. **Understanding the source** - Study the bslib implementation (R, TypeScript, SCSS)
2. **Implementing the port** - Create Python equivalents and add client-side bindings
3. **Testing and documentation** - Ensure correctness with unit and end-to-end tests
## Phase 1: Understanding the Source Implementation
### Step 1.1: Locate the bslib PR
Find the bslib PR that introduced the feature. The py-shiny PR description should reference it (e.g., `Related https://github.com/rstudio/bslib/pull/...`).
**Checklist:**
- [ ] Located the bslib PR
- [ ] Reviewed the PR description and any design discussions
### Step 1.2: Identify the core source files
In the bslib PR, locate these key files:
1. **R implementation**: `R/[feature-name].R` - The main component function(s)
2. **TypeScript bindings**: `srcts/src/components/[featureName].ts` - Client-side behavior
3. **SCSS styles**: `inst/components/scss/[feature_name].scss` - Component styles
4. **Unit tests**: `tests/testthat/test-[feature-name].R` - R unit tests
**Note**: Generated files (compiled JS/CSS, documentation) can be ignored - focus on source files.
**Checklist:**
- [ ] Found the R implementation file(s)
- [ ] Found the TypeScript source file(s)
- [ ] Found the SCSS source file
- [ ] Found the unit test file(s)
- [ ] Reviewed how TypeScript is registered in `srcts/src/components/index.ts`
### Step 1.3: Understand the component structure
Study the bslib implementation to understand:
1. **API design**: Function signature, parameters, defaults
2. **HTML structure**: The DOM elements created by the R function
3. **Client-side behavior**: How the TypeScript binding handles interactions
4. **Shiny communication**: How the component sends/receives values from the server
5. **Dependencies**: What other components or utilities it relies on
**Key patterns to note:**
- Is the component an input binding? If so, how does the binding register (typically via a CSS class)?
- What markup structure does it generate on the server side?
- How does the component integrate with Bootstrap classes?
- How are configuration options passed from R to JavaScript -- data attributes, embedded JSON, etc.?
- How does the component receive data from the server -- via `sendInputMessage()` or `sendCustomMessage()`?
- How does the component receive data from the client -- via `receiveMessage()` or small inputs set with `Shiny.setInputValue()`?
**Checklist:**
- [ ] Documented the component's API surface
- [ ] Understood the HTML structure and CSS classes
- [ ] Identified client-side event handlers and state management
- [ ] Noted any dependencies on other components
## Phase 2: Implementing the Port
### Step 2.1: Create the Python implementation
Create a new file in `shiny/ui/` for the component implementation.
**File naming**: Use snake_case matching the R function name:
- R: `input_submit_textarea()` → Python: `_input_submit_textarea.py`
**Implementation notes:**
- Translate R's htmltools to Python's htmltools
- Use `@add_example()` decorator for documentation examples (examples are created in a later step)
- Follow existing patterns for parameter validation
- Use `resolve_id()` for module namespace support
- Use `restore_input()` for bookmarking support
- Include both the main component function and any `update_*()` functions
**Common translations:**
- R `tags$div()` → Python `div()` or `tags.div()`
- R `!!!args` (splicing) → Python `*args`+`**kwargs`
- R `NULL` → Python `None`
- R lists → Python dicts or lists as appropriate
- R `paste0()` → Python f-strings or `.format()`
**Checklist:**
- [ ] Created `shiny/ui/_[component_name].py`
- [ ] Implemented the main component function with full docstring
- [ ] Implemented any `update_*()` functions
- [ ] Added parameter validation
- [ ] Added support for modules (`resolve_id`)
- [ ] Added support for bookmarking (`restore_input`)
- [ ] Used `components_dependencies()` for client-side deps
### Step 2.2: Export the new functions
Update `shiny/ui/__init__.py` to export the new component:
```python
from ._input_submit_textarea import input_submit_textarea, update_submit_textarea
__all__ = (
# ... existing exports ...
"input_submit_textarea",
"update_submit_textarea",
)
```
If the component is suitable for express mode, also export from `shiny/express/ui/__init__.py`.
**Checklist:**
- [ ] Added imports to `shiny/ui/__init__.py`
- [ ] Added to `__all__` tuple in `shiny/ui/__init__.py`
- [ ] (If applicable) Exported from `shiny/express/ui/__init__.py`
### Step 2.3: Vendor assets from bslib
The TypeScript in bslib is compiled to JavaScript and bundled, and SCSS is compiled to CSS. Py-shiny vendors these compiled assets along with the SCSS source files from bslib.
**Process:**
1. Ensure bslib PR is merged and the feature is in the branch referenced in `scripts/_pkg-sources.R` (usually `@main`)
2. If vendoring from a non-default branch or specific commit, update `scripts/_pkg-sources.R` temporarily
3. Run `make upgrade-html-deps` to vendor the latest assets from bslib, shiny, sass, and htmltools
**What `make upgrade-html-deps` does:**
- Copies SCSS source files from bslib to `shiny/www/shared/sass/bslib/components/scss/`
- Updates all theme preset `_04_rules.scss` files to import the new SCSS
- Compiles SCSS to CSS for all themes
- Vendors compiled JavaScript bundles (`components.min.js`, etc.)
- Updates other shared assets from upstream packages
**Files updated by this process** (examples):
- `shiny/www/shared/sass/bslib/components/scss/[feature_name].scss` (SCSS source)
- `shiny/www/shared/sass/preset/*/04_rules.scss` (27 theme preset files with new imports)
- `shiny/www/shared/bslib/components/components.min.js` (compiled JavaScript)
- `shiny/www/shared/bslib/components/components.min.js.map` (source map)
- `shiny/www/shared/bslib/components/components.css` (compiled CSS)
- Other theme-specific CSS files
**Note**: This is a manual process that's not part of CI. The vendored files are committed to the repository.
**Checklist:**
- [ ] Verified bslib PR is merged (or adjusted `scripts/_pkg-sources.R` if needed)
- [ ] Ran `make upgrade-html-deps`
- [ ] Reviewed changes to vendored files (SCSS, CSS, JS)
- [ ] Verified new SCSS imports in theme preset files
- [ ] Restored `scripts/_pkg-sources.R` if temporarily modified
### Step 2.4: Create API examples
Create example applications demonstrating the component's usage.
**Location**: `shiny/api-examples/[component_name]/`
**Files to create**:
- `app-core.py` - Core mode example
- `app-express.py` - Express mode example (if applicable)
**Example structure**:
```python
# app-express.py
from shiny.express import input, render, ui
ui.input_submit_textarea("text", placeholder="Enter some input...")
@render.text
def value():
if "text" in input:
return f"You entered: {input.text()}"
else:
return "Submit some input to see it here."
```
**Best practices**:
- Keep examples simple and focused
- Demonstrate the primary use case
- Show server-side value handling patterns
- Include any important parameter variations
- Re-use examples from bslib where possible
- Always include an Express version unless there's a strong reason not to
- A human reviewer should test the examples locally
**Checklist:**
- [ ] Created `shiny/api-examples/[component]/app-core.py`
- [ ] Created `shiny/api-examples/[component]/app-express.py` (unless not warranted)
- [ ] Tested examples locally
### Step 2.5: Add Playwright controller (if input component)
If the component is an input component, create a Playwright controller for end-to-end testing.
**Location**: `shiny/playwright/controller/_input_fields.py` (or create new file if needed)
**Implementation**:
1. Create a new class inheriting from appropriate mixins
2. Implement required methods: `__init__`, `set`, interaction methods
3. Add expectation methods for testing (e.g., `expect_value`, `expect_placeholder`)
4. Follow existing patterns for locator initialization
**Example pattern**:
```python
class InputSubmitTextarea(
_SetTextM,
WidthContainerStyleM,
_ExpectTextInputValueM,
_ExpectPlaceholderAttrM,
_ExpectRowsAttrM,
UiWithLabel,
):
"""Controller for :func:`shiny.ui.input_submit_textarea`."""
loc_button: Locator
def __init__(self, page: Page, id: str) -> None:
super().__init__(
page,
id=id,
loc=f"textarea#{id}.form-control",
)
self.loc_button = self.loc_container.locator(".bslib-submit-textarea-btn")
def set(self, value: str, *, submit: bool = False, timeout: Timeout = None) -> None:
# Implementation
pass
```
**Don't forget to export**: Update `shiny/playwright/controller/__init__.py` to export the new controller class.
**Checklist:**
- [ ] Created Playwright controller class
- [ ] Implemented core interaction methods
- [ ] Implemented expectation methods for testing
- [ ] Exported from `shiny/playwright/controller/__init__.py`
- [ ] Added to `__all__` in the same file
## Phase 3: Testing and Documentation
### Step 3.1: Port unit tests from bslib
Port the relevant unit tests from bslib's testthat tests to pytest.
**Translation patterns**:
- R `test_that()` → Python `def test_[name]():`
- R `expect_snapshot()` → Python snapshot testing (if applicable)
- R `expect_error()` → Python `with pytest.raises()`
**Focus on**:
- Parameter validation
- Error messages
- Edge cases
- HTML structure (snapshot tests if helpful)
**Note**: Python has better tooling for end-to-end tests, so unit tests here focus on correctness of the Python API and generated markup.
**Checklist:**
- [ ] Ported relevant unit tests
- [ ] Tests pass locally (`pytest tests/`)
### Step 3.2: Create end-to-end Playwright tests
Create comprehensive end-to-end tests using Playwright. It is likely that these tests do not exist in the R package, so you will need to use your knowledge of the component and its documented behavior to create them. Collaborate with the human reviewer if there are any uncertainties around expected behavior.
**Location**: `tests/playwright/shiny/inputs/[component_name]/`
**Files to create**:
- `app.py` - Test application with multiple variants
- `test_[component_name].py` - Playwright test cases
**Test coverage should include**:
- Initial state verification
- User interactions (typing, clicking, keyboard shortcuts)
- Server updates (via `update_*()` functions)
- Edge cases (empty values, disabled states, etc.)
- Multiple submissions and state changes
**Example test structure**:
```python
def test_input_submit_textarea_initial_state(page: Page, local_app: ShinyAppProc):
page.goto(local_app.url)
basic = controller.InputSubmitTextarea(page, "basic")
basic.expect_label("Enter text")
basic.expect_placeholder("Type something here...")
basic.expect_value("Initial value")
value_output = controller.OutputCode(page, "basic_value")
value_output.expect_value("No value submitted yet")
```
**Best practices**:
- Use the Playwright controller for interactions
- Test both user interactions and programmatic updates
- Use clear, descriptive test names
- Verify both input state and output state
- Output state can be checked by rendering text outputs in the test app
**Checklist:**
- [ ] Created test app with multiple component variations
- [ ] Created comprehensive test cases
- [ ] Tests pass locally (`pytest tests/playwright/`)
- [ ] Covered key interaction patterns and edge cases
### Step 3.3: Update CHANGELOG
Add an entry to `CHANGELOG.md` under the `[UNRELEASED]` section.
**Format**:
```markdown
### New features
* Added `input_new_feature()` [description of function, new features and salient details in 1-3 sentences]. (#[PR_NUMBER])
```
**Checklist:**
- [ ] Added CHANGELOG entry under appropriate section
- [ ] Entry includes clear description and PR reference
### Step 3.4: Run quality checks
Run the various quality checks to ensure your code meets project standards.
**Recommended workflow**:
1. `make formatting` - Fix formatting and linting issues automatically
2. `make check-types` - Run type checking (pyright)
3. `make check-tests` - Run the test suite
4. `make playwright` - Run Playwright end-to-end tests (see `Makefile` for alternate make commands to run subsets of tests)
5. `make check` - Comprehensive checks (slower, runs everything)
**Common issues**:
- Type errors: Ensure proper type hints on all functions
- Format errors: Run `make format` to auto-fix
- Missing imports: Ensure all new modules are properly imported
- Test failures: Debug and fix any failing tests
**Checklist:**
- [ ] `make format` applied successfully
- [ ] `make check-types` passes
- [ ] `make check-tests` passes (or at least your new tests pass)
- [ ] `make playwright` passes (or at least new end-to-end tests pass)
- [ ] Addressed any other linting/quality issues
### Step 3.5: Update API reference configuration
Add the new component functions to the quartodoc YAML configuration files so they appear in the generated documentation.
**Files to update**:
- `docs/_quartodoc-core.yml` - Add component function and update function (if applicable)
- `docs/_quartodoc-express.yml` - Add express versions (if applicable)
- `docs/_quartodoc-testing.yml` - Add Playwright controller (if applicable)
**Where to add entries**:
- Find the appropriate section (e.g., "Inputs" for input components, "Update" for update functions)
- Add entries in alphabetical order within the section
- Use the full module path (e.g., `ui.input_submit_textarea`)
**Example additions**:
```yaml
# In _quartodoc-core.yml under inputs section:
- ui.input_submit_textarea
# In _quartodoc-core.yml under update section:
- ui.update_submit_textarea
# In _quartodoc-express.yml:
- express.ui.input_submit_textarea
- express.ui.update_submit_textarea
# In _quartodoc-testing.yml:
- playwright.controller.InputSubmitTextarea
```
**Checklist:**
- [ ] Added entries to `docs/_quartodoc-core.yml`
- [ ] Added entries to `docs/_quartodoc-express.yml` (if applicable)
- [ ] Added entries to `docs/_quartodoc-testing.yml` (if applicable)
- [ ] Verified alphabetical ordering within sections
### Step 3.6: Build documentation (final step)
Only build documentation at the very end, as it's slow and resource-intensive.
**Command**: `make docs`
**This will**:
- Generate API reference documentation from the quartodoc YAML files
- Build example applications
- Create the documentation website
**Note**: You don't need to run this frequently during development. It's primarily for final verification before PR submission. A human reviewer can handle this entire step, just remind them to run it before merging.
**Checklist:**
- [ ] `make docs` completes successfully
- [ ] Reviewed the generated documentation for your component
- [ ] API reference appears correctly in the appropriate sections
- [ ] Examples are properly linked
## Phase 4: Final Review and Submission
### Step 4.1: Self-review the changes
Before submitting, review all changes:
**Python code**:
- [ ] Functions have comprehensive docstrings
- [ ] Parameter types and defaults match or improve on bslib's API
- [ ] Code follows existing project patterns
- [ ] Error messages are clear and helpful
**Tests**:
- [ ] Unit tests cover parameter validation and edge cases
- [ ] End-to-end tests cover user workflows
- [ ] All tests pass locally
**Styles**:
- [ ] SCSS is imported in all theme presets
- [ ] Styles match the bslib implementation
**Documentation**:
- [ ] CHANGELOG is updated
- [ ] API examples are clear and working
- [ ] Docstrings are complete
### Step 4.2: Create the PR
Create a pull request with:
1. **Title**: `feat: Add [component_name]` or similar
2. **Description**:
- Link to the source bslib PR
- Brief description of the component
- Any implementation notes or decisions
- Example usage
**PR description template**:
```markdown
Related https://github.com/rstudio/bslib/pull/[NUMBER]
Adds `[component_name]`, a new [input/output/UI] component that [brief description].
## Example
Here is a hello world example:
[code block with example]
## Implementation notes
[Any important notes about the implementation, decisions made, etc. or open questions for reviewers.]
```
**Checklist:**
- [ ] PR title follows project conventions
- [ ] PR description links to bslib source
- [ ] PR description includes example usage
- [ ] All quality checks pass in CI
---
## Quick Reference Checklist
Use this high-level checklist to track your progress through the porting process:
### Preparation
- [ ] Located and reviewed the bslib PR
- [ ] Identified all source files (R, TypeScript, SCSS, tests)
- [ ] Understood the component's API and behavior
### Implementation
- [ ] Created Python implementation in `shiny/ui/_[name].py`
- [ ] Exported from `shiny/ui/__init__.py` (and express if applicable)
- [ ] Ran `make upgrade-html-deps` to vendor assets from bslib (SCSS, CSS, JavaScript)
- [ ] Created API examples (core and express)
- [ ] Created Playwright controller (if input component)
### Testing
- [ ] Ported unit tests from bslib
- [ ] Created comprehensive Playwright tests
- [ ] Updated CHANGELOG
### Documentation
- [ ] Updated quartodoc YAML files to include new functions
- [ ] Ran `make format`
- [ ] Passed `make check-types`
- [ ] Passed `make check-tests`
- [ ] Built docs with `make docs`
### Submission
- [ ] Self-reviewed all changes
- [ ] Created PR with proper description
- [ ] All CI checks passing
---
## Common Patterns and Tips
### Handling component dependencies
If the component depends on other components:
- Use `components_dependencies()` for bslib components (automatic)
- For other dependencies, add them explicitly in the component function
### Testing with different themes
The component should work with all Bootstrap themes. The `make upgrade-html-deps` process ensures SCSS is properly imported across all theme presets, but you may want to spot-check a few themes manually during development.
### Parameter naming conventions
Python style guide preferences:
- Use `snake_case` for parameter names
- Match bslib's parameter names when possible
- Use `Optional[T]` for user-facing parameters that can be `None`
- Use `Literal` rather than `Enum` for parameters with specific allowed values
### Working with htmltools
Key patterns:
- `Tag` objects are mutable by default, use `copy.copy()` if you need to modify
- Use `.add_class()`, `.add_style()`, `.attrs` to add CSS classes, styles, and HTML attributes
- Create new tags with `ui.tags` and add attributes and children directly, e.g. `ui.tags.div(class_="fw-bold", ...)`
- Use `css()` helper for inline styles
- Tag children can be strings, Tags, TagLists, or None
---
## Troubleshooting
### "Tests fail with 'element not found'"
- Verify Playwright selectors match the actual HTML structure
- Check that the component is properly rendered before interaction
- Use `page.wait_for_selector()` if there are timing issues
### "SCSS not taking effect"
- Check that `make upgrade-html-deps` was run to vendor and compile SCSS/CSS
- Verify the SCSS file exists in `shiny/www/shared/sass/bslib/components/scss/`
- Verify the imports were added to preset `_04_rules.scss` files (should be automatic)
- Clear browser cache when testing
---
## Additional Resources
- **bslib repository**: https://github.com/rstudio/bslib
- **py-shiny repository**: https://github.com/posit-dev/py-shiny
- **Example PR (bslib)**: https://github.com/rstudio/bslib/pull/1204
- **Example PR (py-shiny)**: https://github.com/posit-dev/py-shiny/pull/2099
For questions or clarifications, consult with the Shiny team or reference previous component ports for patterns.