Skip to content

Example: Custom Component

Build a GaugeChart that renders on an HTML <canvas> using pure JavaScript — no external charting library required. See examples/custom_component.py in the repo for the full runnable file.


custom_component.py
import fluxui as ui
from fluxui.components.advanced import CustomComponent


class GaugeChart(CustomComponent):
    """
    A semicircular gauge chart drawn on a <canvas> element.
    No external scripts required — pure Canvas 2D API.
    """

    def __init__(self, value, *, max_value: int = 100, label: str = "") -> None:
        self._value     = value      # ReactiveVar[float] or plain number
        self._max_value = max_value
        self._label     = label

    def _resolve(self, v):
        """Unwrap a ReactiveVar if needed."""
        return v.value if hasattr(v, "value") else v

    def render(self) -> str:          # (1) called every time the component re-renders
        val = self._resolve(self._value)
        return (
            f'<div style="display:flex;flex-direction:column;align-items:center;'
            f'padding:1rem;">'
            f'  <canvas width="200" height="120"'
            f'          data-value="{val}"'
            f'          data-max="{self._max_value}"'
            f'          data-label="{self._label}">'
            f'  </canvas>'
            f'</div>'
        )

    def client_script(self) -> str:   # (2) runs in the browser after render
        return r"""
        (function() {
            var canvas = this.querySelector('canvas');
            if (!canvas || !canvas.getContext) return;
            var ctx   = canvas.getContext('2d');
            var value = parseFloat(canvas.dataset.value);
            var max   = parseFloat(canvas.dataset.max);
            var label = canvas.dataset.label;
            var pct   = Math.min(1, Math.max(0, value / max));
            var W = canvas.width, H = canvas.height;
            var cx = W / 2, cy = H - 10, r = 75;

            ctx.clearRect(0, 0, W, H);

            // Background track
            ctx.beginPath();
            ctx.arc(cx, cy, r, Math.PI, 0, false);
            ctx.lineWidth = 16;
            ctx.strokeStyle = getComputedStyle(document.documentElement)
                               .getPropertyValue('--flux-border') || '#e0e0e0';
            ctx.stroke();

            // Value arc
            if (pct > 0) {
                ctx.beginPath();
                ctx.arc(cx, cy, r, Math.PI, Math.PI + pct * Math.PI, false);
                ctx.lineWidth = 16;
                ctx.lineCap   = 'round';
                ctx.strokeStyle = pct > 0.75 ? '#5f8a5f'
                                : pct > 0.40 ? '#d4a44a'
                                : '#c0625a';
                ctx.stroke();
            }

            // Value text
            var fg = getComputedStyle(document.documentElement)
                      .getPropertyValue('--flux-text') || '#333';
            ctx.fillStyle  = fg;
            ctx.font       = 'bold 24px Inter, system-ui, sans-serif';
            ctx.textAlign  = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText(Math.round(value), cx, cy - 14);

            // Label text
            ctx.font      = '12px Inter, system-ui, sans-serif';
            ctx.fillStyle = getComputedStyle(document.documentElement)
                             .getPropertyValue('--flux-text-muted') || '#888';
            ctx.fillText(label, cx, cy + 4);
        }).call(this);
        """

    def scripts(self) -> list:        # (3) no external scripts needed
        return []


# ── Demo page ─────────────────────────────────────────────────────────────────

app = ui.App("Custom Component Demo")


@app.page("/")
def home(session):
    gauge_value = ui.state(65)
    dark_mode   = ui.state(False)

    async def toggle_theme(v: bool) -> None:
        dark_mode.set(v)
        await session.set_theme("dark" if v else "light")

    with ui.Column(gap="var(--s-6)"):
        with ui.Row(justify="space-between", align="center"):
            ui.Heading("Custom Gauge Component", level=1)
            ui.Switch("Dark mode", checked=dark_mode, on_change=toggle_theme)

        ui.Text(
            "This gauge is a CustomComponent subclass — it renders an HTML canvas "
            "and draws on it with the Canvas 2D API via client_script().",
            variant="muted",
        )

        with ui.Grid(cols=3, gap="var(--s-4)"):
            GaugeChart(gauge_value,          max_value=100, label="Score")
            GaugeChart(lambda: gauge_value.value * 0.7, max_value=100, label="Efficiency")
            GaugeChart(lambda: 100 - gauge_value.value, max_value=100, label="Headroom")

        with ui.Card(title="Adjust value"):
            ui.Slider(
                label="Gauge value",
                value=gauge_value,
                on_change=gauge_value.set,
                min=0,
                max=100,
                step=1,
            )
            ui.Text(lambda: f"Current value: {int(gauge_value.value)}", variant="caption")

        with ui.Card(title="How it works"):
            ui.Code(
                "class GaugeChart(CustomComponent):\n"
                "    def render(self) -> str:\n"
                "        # Returns HTML with data-* attributes\n"
                "        ...\n\n"
                "    def client_script(self) -> str:\n"
                "        # JavaScript that runs after render()\n"
                "        # `this` = the component's root DOM element\n"
                "        ...\n\n"
                "    def scripts(self) -> list[str]:\n"
                "        # External <script> URLs (deduplicated across components)\n"
                "        return []",
                language="python",
            )


if __name__ == "__main__":
    app.run(port=8000)
  1. render() returns an HTML string. FluxUI wraps it in a <div data-flux-id="..."> and sends it to the browser. The data-* attributes carry the value and label.
  2. client_script() returns a JavaScript string executed with this bound to the component's root element. It reads the data-* attributes and draws the arc on the canvas. Because render() is called again when gauge_value changes, the canvas is redrawn automatically.
  3. scripts() lists external script URLs to inject. FluxUI deduplicates them — if 3 gauges are on one page, the script is injected only once.

Run it

pip install fluxui
python custom_component.py

Drag the slider — all three gauges update in real time.