holoviz / panel

Panel: The powerful data exploration & web app framework for Python
https://panel.holoviz.org
BSD 3-Clause "New" or "Revised" License
4.46k stars 488 forks source link

CSS Variables set inside components #5321

Open MarcSkovMadsen opened 11 months ago

MarcSkovMadsen commented 11 months ago

I wanted to increase the font-size a bit for a mobile app. It led me to look at how css variables are used in Panel.

I believe that they are used in a fundamentally wrong way. I see this as a bug.

My understanding of css variables is that they are there to enable users to tweek the look at feel of components from outside the component/ shadowRoot. But in Panel I see them defined inside the component/ shadowRoot.

Please fix this and define the variables once on the template level. And make it such that they can be overridden once when using custom templates or using Panel with index.html+styles.css files in pyscript.com. My request is really to adhere to the best practices from the mdn docs https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties.

Example issue 1 - Cannot change the look and feel for all components inside a container

You cannot change the look and feel for all components inside a container.

import panel as pn

CSS = """
:host, :root {
    --panel-primary-color: salmon;
}
"""

pn.Column(
    pn.widgets.Button(name="Button 1", button_type="primary"),
    pn.widgets.Button(name="Button 2", button_type="primary"),
    stylesheets=[CSS],
).servable()

image

Workaround

You will have to change the css variable on all the components

import panel as pn

CSS = """
:host, :root {
    --panel-primary-color: salmon;
}
"""

pn.Column(
    pn.widgets.Button(name="Button 1", button_type="primary", stylesheets=[CSS]),
    pn.widgets.Button(name="Button 2", button_type="primary", stylesheets=[CSS]),
    stylesheets=[CSS],
).servable()

image

Alternatively you can hack your way around this using !important or * instead of :host. But this is really a hack. Not the right way to do it.

Example 2 - Pyscript

When using Panel with pyscript I would expect it to be possible to define/ override css variables once via a stylesheet.css file that is imported in the head section. But this is not possible.

This just shows how difficult it will be to integrate Panel into a larger app whether its pyscript, flask, django etc. It should be possible to let the data scientist create the data app. And the the web developer/ designer style the app from the outside via css variables.

Example 3 - Light/ Dark theme

We would really like to be able to quickly change between light and dark theme. Today we do this by reloading the page. But really it should be possible by just changing a class on the body element or a high level container. Alternatively replacing one .css file.

Best Practices for CSS Custom Properties

See https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties.

image

image

Documentation

When implemented the documentation below should be updated.

image

CSS Variables should be configured globally or on a container. Can also be configured on a component, but that should be a less used use case.

Additional Inspiration

I look to Shoelace and Fast.design to for reference implementations using Shadow dom.

Its worth looking into the way themes a defined in Shoelace. Not on the component. But inside classes. This makes it really easy to change the theme or apply themes to parts of a page.

Its also worth looking into using Constructable Stylesheets as Fast does. See https://web.dev/constructable-stylesheets/#:~:text=Constructable%20Stylesheets%20make%20it%20possible,Document%20easily%20and%20without%20duplication.

MarcSkovMadsen commented 11 months ago

In https://discourse.holoviz.org/t/how-to-change-font-in-pn-pane-str/5788 Jan asks for help styling. The solution is

import panel as pn

pn.extension()

STR_STYLE_SHEET = """
:host {
    --bokeh-mono-font: Times;
}
"""

pn.pane.Str(
    "This is a raw string which \n"
    "will not be formatted in any way \n"
    "except for the applied style.",
    styles={"font-size": "20pt", "line-height": "120%", "letter-spacing": "-1px"},
    stylesheets=[STR_STYLE_SHEET],
).servable()

This is an example where

ahuang11 commented 11 months ago

I also want to empathize that styling components is a fairly tedious and time-consuming process.

I too thought your Example issue 1 should have worked, but ended up having to inject stylesheets on each component.

rsdenijs commented 10 months ago

Fyi one solution for this is to subclass the components you want to style

import panel as pn

slider_css = """
/* Show a border when hovering the area the handle responds to */
:host {
  --handle-width: 15px;
  --handle-height: 25px;
  --slider-size: 25px;
}
"""

class MySlider(pn.widgets.FloatSlider):
    stylesheets = [slider_css]

slider1 = MySlider(name="Number")

slider2 = pn.widgets.FloatSlider(name="Number")

col = pn.Column(slider1, slider2)

col.servable()

You can use MySlider throughout your app without having to pass in stylesheets all around. If you know you want to affect all sliders, you can assign the .stylesheets property of the FloatSlider directly.

jclevesque commented 9 months ago

Stumbled here trying to apply a CSS style block to my whole page, seems like it shouldn't be that hard?

ahuang11 commented 9 months ago

On Discord, @maximlt suggested doing:

class MyDesign(Design):

    modifiers = {
        Viewable: {
            'stylesheets': [Inherit, 'assets/myglobalstyle.css']
        }
    }

pn.config.design = MyDesign

Maybe this should be documented if it's the proper thing to do.

MarcSkovMadsen commented 9 months ago

Sounds like a good idea to document. But be aware that if you want to modify a Bootstrap or Material design you probably need to extend their modifiers.

peytondmurray commented 9 months ago

Are there performance implications for this? I'm building an app with many widgets, and the DOM is cluttered with included stylesheets, and I'm guessing it's slow to render because of this.

ahuang11 commented 9 months ago

In that thread, Philipp stated: """ I think unless you are using inline CSS you should not worry very much about performance. Stylesheets that are loaded from the server are cached in browser, so producing a single CSS file that is applied everywhere is okay to start with and can always be broken up later. So I'd say in your case I think a Design would be quite reasonable. """

philippjfr commented 9 months ago

Are there performance implications for this? I'm building an app with many widgets, and the DOM is cluttered with included stylesheets, and I'm guessing it's slow to render because of this.

I would be very surprised if this was the case unless you were injecting many stylesheets with thousands of lines into every single component. I guess I'd have to see exactly what you were doing though. Are these stylesheets inlined or loaded from the server?

rsdenijs commented 8 months ago

After not managing to forward the css variables explicitly into the shadow DOM I went this route too. It feels a bit hacky but to @MarcSkovMadsen 's point, what worked for me was

my_template.design.modifiers[Viewable]["stylesheets"] += raw_css

peytondmurray commented 8 months ago

I would be very surprised if this was the case unless you were injecting many stylesheets with thousands of lines into every single component. I guess I'd have to see exactly what you were doing though. Are these stylesheets inlined or loaded from the server?

Sorry I've been slow to get back to this. When I posted the comment above I wasn't yet using inline styles. Still, every element was including identical styles throughout the document:

image

At the time of my previous post, I was rendering ~120 EditableFloatSlider and EditableRangeSlider widgets each of which are comprised of multiple subwidgets which carry their own styles with them, along with some markdown, rows, dividers, columns, etc. Opening the developer console in FF to inspect the DOM took so long it was basically impossible to modify styles for the document.


At the moment, I am instead using ~120 FloatInput widgets, and the performance is much better - presumably because they aren't made up of ~10 subwidgets that carry their own styles. I can now inspect the DOM (there's still a ~2s delay opening it) though, so that's an improvement but still not ideal.

More recently, I've worked on styling the UI, which meant adding styles and stylesheets to the widgets I'm using. I've noticed that since these changes, the performance of the UI is significantly worse.

rsdenijs commented 8 months ago

@peytondmurray Not directly related to the issue, but if you have performance issues you could group some of those 120 widgets inside sub layouts (column, card, etc) and only render the section that is relevant.