Skip to content

[feat] Add custom hex color support for markdown rendering#14041

Merged
lukasmasuch merged 5 commits intodevelopfrom
lukasmasuch/custom-color-directive
Feb 20, 2026
Merged

[feat] Add custom hex color support for markdown rendering#14041
lukasmasuch merged 5 commits intodevelopfrom
lukasmasuch/custom-color-directive

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Feb 20, 2026

Describe your changes

  • Add support for :color[text]{foreground="..." background="..."} syntax in markdown, enabling custom CSS colors (hex, rgb, rgba, hsl, hsla, named colors) for text styling
  • Add secure isValidCssColor() validation function to prevent XSS via CSS injection
  • Reuses existing stMarkdownColoredText and stMarkdownColoredBackground CSS classes for consistent styling

Github issues

Testing Plan

  • Unit Tests (JS) - Comprehensive tests for isValidCssColor() validation and custom color directive rendering
  • E2E Tests - Visual regression tests for foreground, background, combined colors, rgb(), and named color formats- [ ]

Add support for `:color[text]{foreground="..." background="..."}` syntax
in markdown, enabling users to specify arbitrary CSS colors (hex, rgb,
rgba, hsl, hsla, named colors) for text styling.

- Add isValidCssColor() function with secure validation against XSS
- Handle custom color directive in createRemarkColoringAndSmall plugin
- Add comprehensive unit tests for color validation and directive
- Add E2E tests for visual regression coverage
Copilot AI review requested due to automatic review settings February 20, 2026 13:55
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Feb 20, 2026
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Feb 20, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 20, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-14041/streamlit-1.54.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-14041.streamlit.app (☁️ Deploy here if not accessible)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new markdown text directive to Streamlit’s frontend markdown renderer to allow user-specified foreground/background colors via :color[...] attributes, along with corresponding unit + e2e coverage.

Changes:

  • Implement :color[text]{foreground="..." background="..."} handling in the remark directive pipeline, reusing existing colored-text/background CSS classes.
  • Add isValidCssColor() to validate user-provided color strings before injecting them into an inline style attribute.
  • Add frontend unit tests + extend st_markdown E2E app/tests to cover the new directive.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx Adds CSS color validation + directive handling that renders styled <span> elements.
frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx Adds unit tests for isValidCssColor and rendering behavior for the new directive.
e2e_playwright/st_markdown.py Adds new demo cases to the markdown E2E app script for the custom color directive.
e2e_playwright/st_markdown_test.py Adds E2E assertions (and one snapshot) verifying the directive renders expected styles.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

@lukasmasuch lukasmasuch added the update-snapshots Trigger snapshot autofix workflow label Feb 20, 2026
@github-actions github-actions bot removed the update-snapshots Trigger snapshot autofix workflow label Feb 20, 2026
## Describe your changes

Automated snapshot updates for #14041 created via the snapshot autofix
CI workflow.

This workflow was triggered by adding the `update-snapshots` label to a
PR after Playwright E2E tests failed with snapshot mismatches.

**Updated snapshots:** 6 file(s)

⚠️ **Please review the snapshot changes carefully** - they could mask
visual bugs if accepted blindly.

This PR targets a feature branch and can be merged without review
approval.

Co-authored-by: Streamlit Bot <[email protected]>
@lukasmasuch lukasmasuch changed the title [feat] Add custom color directive for st.markdown [feat] Add custom color support for markdown rendering Feb 20, 2026
Move custom color directive testing into the existing mixed_markdown
section instead of having separate dedicated tests. This reduces
test maintenance overhead while still providing visual regression
coverage through the mixed_markdown snapshot.
@lukasmasuch lukasmasuch changed the title [feat] Add custom color support for markdown rendering [feat] Add custom hex color support for markdown rendering Feb 20, 2026
Invalid color values in :color[] now render the content as a plain span
instead of falling through to unsupported directive cleanup which would
lose the content text. Updated tests to verify this behavior.
@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Feb 20, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds support for a custom CSS color directive in Streamlit's markdown rendering. Users can now write :color[text]{foreground="#FF5733" background="#000000"} to apply arbitrary CSS colors (hex, rgb, rgba, hsl, hsla, named colors) to text foreground and background. The implementation:

  • Adds a new isValidCssColor() validation function that delegates to color2k's parseToRgba (an existing project dependency) for secure color validation.
  • Extends the createRemarkColoringAndSmall remark plugin to handle the :color directive with foreground and background attributes.
  • Reuses existing stMarkdownColoredText and stMarkdownColoredBackground CSS classes for consistent styling.
  • Adds comprehensive unit tests and updates E2E snapshots for visual regression coverage.

Code Quality

The code is clean, well-structured, and follows the existing patterns in StreamlitMarkdown.tsx. Specific observations:

  1. Good pattern reuse: The new handler mirrors the structure of the existing color directive handlers (setting data.hName, data.hProperties, etc.) and reuses the same CSS classes (stMarkdownColoredText, stMarkdownColoredBackground).

  2. Placement is correct: The new handler is placed before the colorMapping.has(nodeName) check at line 746, ensuring that :color[text]{foreground="..."} is caught before reaching the named-color handler. Since "color" is not a key in colorMapping, this doesn't change existing behavior for predefined color names like :red[] or :blue[].

  3. Inline style usage: The frontend AGENTS.md recommends avoiding inline style props in favor of Emotion styled components. However, this code operates at the HAST (markdown AST) level during remark processing, where Emotion components aren't applicable. The existing predefined-color handler uses the exact same data.hProperties.style approach (line 751), so this is consistent with the codebase.

  4. Minor note on node.attributes check (line 682): In the remark-directive AST, node.attributes is always an object (possibly empty {}) for text directives, making the && node.attributes check always truthy when nodeName === "color". This means :color[text] without any attributes will also be intercepted by this handler. The resulting behavior (rendering as a plain <span>) is actually better than the previous behavior (the unsupported-directive cleanup would mangle it to just :color, losing the [text] content). This is a minor improvement, not a bug.

Test Coverage

Unit tests are well-done:

  • isValidCssColor: Thorough parameterized tests covering hex (3/4/6/8-digit), rgb/rgba, hsl/hsla, named colors, empty string, invalid formats, and security attack vectors (XSS, CSS expressions).
  • Custom color directive rendering: Tests for foreground-only, background-only, both colors, 3-digit hex, named colors, invalid color graceful degradation, and XSS rejection.

E2E tests: The custom color examples are added to the mixed markdown block (st_markdown.py line 259-260), which is covered by the themed snapshot test test_many_elements_in_one_block. Updated snapshots across all three browsers and both themes.

Potential gaps (minor, non-blocking):

  • No unit test for :color[text]{foreground="red" background="notacolor"} (one valid, one invalid) — verifying that partial validity still applies the valid color correctly. The implementation handles this correctly, but a test would document the expected behavior.
  • No unit test for rgb() format in the directive (e.g., :color[text]{foreground="rgb(255,0,0)"}), though isValidCssColor tests do cover rgb() validation.
  • No unit test for :color[text] or :color[text]{} (no attributes) — confirming content is preserved as a plain span.

Backwards Compatibility

No breaking changes. The existing named-color directives (:red[], :blue[], etc.) and their background variants are unaffected because:

  • The new handler only intercepts nodeName === "color", and "color" is not a key in the colorMapping.
  • The new handler runs before the colorMapping.has(nodeName) check, but returns early so it doesn't interfere with subsequent handlers.

The only behavioral change is for the edge case :color[text] (without attributes), which previously rendered as the literal text :color (losing the bracket content). It now renders the full text content in a <span>, which is an improvement.

Security & Risk

Security is well-addressed:

  • The isValidCssColor() function (line 651-658) delegates to color2k's parseToRgba(), which strictly parses only valid CSS color values. It rejects anything containing semicolons, url(), expression(), var(), javascript:, or other non-color content.
  • Validated color values are interpolated into property-specific CSS declarations (color: ${foreground}, background-color: ${background}), limiting the injection surface further.
  • The test suite explicitly covers javascript:alert(1) and expression(alert(1)) attack vectors.
  • color2k is an existing, widely-used dependency already in the project (used across 20+ files for theme operations).

Risk assessment: Low. The validation approach is defense-in-depth: even if a color string somehow passed validation, it would be placed in a color: or background-color: CSS property context, limiting exploitability. The PR has a security-assessment-completed label.

Accessibility

The custom color directive uses the same HTML structure and CSS classes as the existing predefined color directives — <span> elements with stMarkdownColoredText or stMarkdownColoredBackground classes. No new interactive elements are introduced.

One consideration: custom foreground/background colors could create contrast issues for users with visual impairments (e.g., light text on light background). However, this is inherent to the feature request and is the author's responsibility when using custom colors, similar to how raw CSS works. No automated enforcement is practical here.

Recommendations

  1. Consider adding a unit test for mixed valid/invalid attributes to document the partial-validity behavior. For example, :color[text]{foreground="red" background="notacolor"} should apply only the foreground color and use the stMarkdownColoredText class. This is non-blocking since the implementation handles it correctly.

  2. Consider adding a unit test for :color[text] without attributes (or with empty attributes {}). This edge case now renders as a plain <span> (content preserved), which is better than the previous behavior, but documenting it in a test would be valuable.

  3. (Nit) The condition on line 682 nodeName === "color" && node.attributes could be simplified to just nodeName === "color" since node.attributes is always truthy for remark-directive text directives. However, keeping it adds clarity about the intent (this handler is for the attribute-based syntax), so this is purely a style observation.

Verdict

APPROVED: Clean, well-tested implementation that adds custom CSS color support to markdown directives with solid security validation via an existing dependency. The code follows existing patterns, introduces no breaking changes, and has comprehensive unit test coverage.


This is an automated AI review by opus-4.6-thinking.

Add tests documenting behavior for:
- Mixed valid/invalid attributes (only valid colors applied)
- Empty attributes (renders as plain span)
@lukasmasuch lukasmasuch merged commit 73a7081 into develop Feb 20, 2026
43 of 44 checks passed
@lukasmasuch lukasmasuch deleted the lukasmasuch/custom-color-directive branch February 20, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support :hex[text to be colored] for colored text in addition to having :color[text to be colored]

3 participants