Skip to main content

richframe

richframe turns pandas.DataFrame objects into richly styled HTML tables with theming, formatting, and layout control.

Installation

pip install richframe
# or with uv
uv pip install richframe

From source (repository root):

pip install -e .
# or with uv
uv pip install -e .

Quick start

import pandas as pd
from richframe import to_html

sales = pd.DataFrame(
    {
        "Region": ["North", "South", "West", "East"],
        "Units": [120, 85, 102, 150],
        "Growth": [0.12, -0.05, 0.08, 0.21],
    },
    index=pd.Index(["Q1", "Q2", "Q3", "Q4"], name="Quarter"),
)

html = to_html(
    value=sales,
    theme="light",
    caption="Quarterly Sales",
    formatters={"Units": "number", "Growth": "percent"},
)

Render the html string in a Jupyter notebook using IPython.display.HTML(html) or embed it in any HTML-aware surface.

Capabilities

  • Core Rendering — Transform DataFrames into a structured Table model and emit accessible HTML through Jinja templates.
  • Theme & Style System — Ship Minimal, Light, and Dark themes with class deduplication and an inline CSS mode for email-compatible output.
  • Formatting Toolkit — Apply built-in number, currency, percentage, and date formatters, optionally locale-aware via the babel extra.
  • Layout Controls — Configure widths, alignment, visibility, sticky columns, sticky headers, zebra striping, and rule-driven row styling.
  • Interactive Controls — Opt into client-side column filtering, ASC/DESC sorting, and drag-to-resize handles with interactive_controls=True and resizable_columns=True.
  • Intelligent Merging — Derive row/column spans for MultiIndex headers and indexes while preserving accessibility via scope and headers metadata.
  • Plugin Layer — Compose color scales, in-cell data bars, icon sets, and fluent conditional formatting through a lightweight plugin pipeline.

Layout & styling examples

from typing import Sequence
from richframe import ColumnConfig, RowStyle, to_html

highlight = RowStyle(background_color="#fff3cd")

def high_growth(index: str, values: Sequence[object]) -> bool:
    return values[2] is not None and values[2] > 0.15

html = to_html(
    value=sales,
    theme="dark",
    title="Regional Performance",
    subtitle="FY24 Snapshot",
    column_layout={
        "Quarter": ColumnConfig(id="Quarter", sticky=True, width="110px"),
        "Region": {"width": "140px"},
        "Units": {"align": "right"},
        "Growth": {"align": "right"},
    },
    formatters={"Units": "number", "Growth": "percent"},
    sticky_header=True,
    zebra_striping=True,
    row_predicates=[(high_growth, highlight)],
)

The renderer automatically adjusts zebra striping and sticky column offsets to maintain readable output across themes.

Interactive controls

richframe ships optional in-browser controls so viewers can explore tables without leaving the page. Toggle them when calling to_html():

html = to_html(
    value=sales,
    interactive_controls=True,
    resizable_columns=True,
)

Each header displays a dropdown icon that opens searchable filters, ASC/DESC sort buttons, and a “Reset” action. A slim handle on the header’s right edge lets users drag to resize the column.

You can embed the same experience inside a Streamlit app:

import streamlit as st
import streamlit.components.v1 as components
import pandas as pd
from richframe import to_html

sales = pd.DataFrame(
    {
        "Region": ["North", "East", "South", "West"],
        "Quarter": ["Q1", "Q1", "Q2", "Q3"],
        "Revenue": [120000, 98000, 143000, 110000],
    }
)

html = to_html(
    value=sales,
    theme="dark",
    include_index=False,
    interactive_controls=True,
    resizable_columns=True,
)

components.html(html, scrolling=True, height=600, width=800)

Layout best practices

  • Wrap tables responsively: the default richframe-container adds horizontal scrolling when needed; keep it in place when embedding inside cards or panes.
  • Combine sticky headers with zebra striping: stripes adapt to light/dark backgrounds, and inline sticky positioning prevents header bleed in emails.
  • Assign widths to sticky columns: provide explicit pixel widths (e.g. ColumnConfig(width="120px", sticky=True)) to minimise layout jitter; a 120px fallback is used when omitted.
  • Use row predicates sparingly: pair them with named RowStyle instances for reuse across tables and plugins.
  • Derive themes instead of duplicating: call compose_theme("light", name="brand", header_cell_style={"background_color": "#0f172a"}) and register it once with register_theme.

MultiIndex merging example

columns = pd.MultiIndex.from_tuples(
    [
        ("North", "Retail", "Q1"),
        ("North", "Retail", "Q2"),
        ("North", "Wholesale", "Q1"),
        ("South", "Retail", "Q1"),
    ],
    names=["Region", "Channel", "Quarter"],
)
index = pd.MultiIndex.from_tuples(
    [
        ("North", "Austin", "Store 1"),
        ("North", "Austin", "Store 2"),
        ("North", "Dallas", "Store 3"),
        ("South", "Houston", "Store 4"),
    ],
    names=["Region", "City", "Store"],
)

pivot = pd.DataFrame(
    [
        [10, 12, 8, 7],
        [9, 11, 7, 6],
        [13, 15, 9, 8],
        [14, 16, 10, 9],
    ],
    index=index,
    columns=columns,
)

html = to_html(value=pivot, theme="minimal", sticky_header=True, zebra_striping=True)

richframe merges repeated labels in both the header and index hierarchies, emits accurate scope/headers metadata, and keeps merged cells sticky when requested.

Conditional styling plugins

from richframe import (
    ColorScalePlugin,
    DataBarPlugin,
    IconRule,
    IconSetPlugin,
    conditional_format,
    to_html,
)

sales = pd.DataFrame(
    {
        "Region": ["North", "South", "West", "East"],
        "Units": [120, 85, 102, 150],
        "Growth": [0.12, -0.05, 0.08, 0.27],
    },
    index=pd.Index(["Q1", "Q2", "Q3", "Q4"], name="Quarter"),
)

plugins = [
    ColorScalePlugin("Growth", palette=("#ecfccb", "#15803d")),
    DataBarPlugin("Units"),
    IconSetPlugin(
        "Growth",
        rules=(
            IconRule(
                lambda v: isinstance(v, (int, float)) and v > 0.1,
                "🔺",
                {"color": "#16a34a"},
            ),
            IconRule(
                lambda v: isinstance(v, (int, float)) and v <= 0.0,
                "🔻",
                {"color": "#dc2626"},
            ),

        ),
    ),
    conditional_format()
    .when(
        column="Growth",
        predicate=lambda v: isinstance(v, (int, float)) and v > 0.2,
    )
    .style(border_bottom="2px solid #16a34a"),
]

html = to_html(
    value=sales,
    theme="light",
    inline_styles=True,
    plugins=plugins,
    title="Regional Performance",
    subtitle="Plugin showcase",
)

Plugins run after formatting and theming, letting you combine visual cues such as heatmaps, data bars, and icons without losing theme defaults.

Testing

uv run pytest
# Snapshot-only smoke
uv run pytest -m snapshot
# Skip performance baselines
uv run pytest -m "not performance"

Built with pandas and jinja2.

Metadata

Release files for richframe 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for richframe 0.3.0
File Size Uploaded Provenance Actions
richframe-0.3.0.tar.gz 35.2 kB Without provenance Details for richframe-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for richframe 0.3.0
File Interpreter ABI Platform Actions
richframe-0.3.0-py3-none-any.whl Python 3 none any Details for richframe-0.3.0-py3-none-any.whl

Total release size: 83.1 kB

Release files / richframe-0.3.0.tar.gz

Download URL richframe-0.3.0.tar.gz
Size 35.2 kB
Tags Source
SHA-256 checksum
How to use checksums
7c85625d250fcb558831ef6eece18ec40cb8a666a61aa105553e3d5adafc0a4a
BLAKE2b-256 checksum
How to use checksums
c251460b7a9421ce5a6a42a8ee3a996048c18101cfb85c33876e2cee787f82aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.14

Release files / richframe-0.3.0-py3-none-any.whl

Download URL richframe-0.3.0-py3-none-any.whl
Size 48.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
790664d7e762cf3f03ef4963af26d6a910e08e25f440ee00b588869650968592
BLAKE2b-256 checksum
How to use checksums
1ca65aa2c7bd695cce3395289832eefbe239fca94347825b98823df1789107bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.14

Security for richframe 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page