Allow Custom CSS#1170
Conversation
|
Interesting! I like that it allows you to apply css styles directly but also take advantage of type hints. Some thoughts:
So in addition to what you are saying, perhaps every component that needs to can define its own inner class class Image():
class CSS(gr.CSS):
def __init__(editButtonCss=None, **kwargs)
....And then you would do:
|
|
What you're saying makes sense. We could implement as you suggested, or how about this: In this approach, no more nesting, and only one gr.CSS class. There is a CSS method to each component, and each component adds kwargs for each possible target. Type hints available at every step. |
|
Nice, that's pretty clean |
This is a feature not a bug. It means we can pretty much guarantee that components will always work as expected. There is already an escape hatch in the global CSS (readded in this PR). We could easily add an I personally don't like this approach due to how much it exposes and how little control we have. A first-class API for changing every css property means we explicitly support this (and we will need to provide support to people having CSS issues). I also don't like the fact that this design uses the js property camelcased notation which will not be familiar to users who have a passing familiarity with CSS but little JS knowledge (the camel -> kebab conversion is frequently confusing for js devs). It will also be confusing when users look up css properties and need to convert them to camel case. |
|
I also don't think Additionally, even quite granular concepts like 'edit button' are frequently made up of multiple elements. Something like a clear button on an image might be made up of 2 or 3 elements. A wrapper div might handle margins/spacing, a button might handle borders, shadows, radiuses; and a final element (svg perhaps) might handle the actual icons, line colors, line width, image size/ margins, etc. Even the granularly targeted CSS kwarg cannot address this. We could need several. And this is just one small thing. The media components have several buttons, a container, the core component itself, the upload functionality, the empty inner text (two kinds). We would need at least half a dozen targetted This approach also means that any minor changes we make to the structure of the markup are essentially breaking changes. We will be stuck between not making changes to the markup (which is an implementation detail) or frequently breaking user-provided CSS. Something that we have had feedback about in v2 and wanted to avoid in v3. |
|
I see. So what you're suggesting is:
Btw there's not good supported type-hinting within dictionaries in Python. So would not provide any type-hinting from the style= kwarg. That's why I was suggesting an extra method with kwargs, e.g. |
|
I don't really mind what the specific API is really, I just think it should be an abstraction so we have some control over a) what we allow, and b) what we do with it. That alongside an escape hatch should give us the flexibility to allow some custom styling without exposing implementation details but still allow users to hack on the CSS if they really want to. In terms of accessing the components more directly, I'd propose adding a standard We could also allow users to pass an options But even with just the classes, user's would have enough of an escape hatch to do almost anything without us 'blessing' it with an official API. We can document it as an escape hatch and make no promises about not breaking user's CSS. I think my main motivation here is to keep this as light touch as possible until we see where the limitations are. A granular API, with only a few things exposed is something we can release + test. If it works well we can look at use-cases and decide what else to add. If it doesn't work well or isn't useful we can pause there without any great harm. Exposing everything by default is more difficult to walk back and would require a 4.0. |
|
Regarding the API, a fluent interface strikes me as a little odd in gradio (do we use that pattern anywhere else?) but I'm not against it. I'd be fine with a |
There was a problem hiding this comment.
This looks good and the PR comments capture my feedback mostly, but we need to avoid passing the CSS down directly to elements as raw CSS for a few reasons.
- Firstly, it makes breakages more likely (we use lots of flex and absolute positioning, it would be very easy to break layouts and we want to stay in control, gradio apps should almost always render correctly when using style kwargs)
- Secondly, it won't actually be enough to style some things. When we receive certain css properties we may actually need to modify multiple elements not just one, so simply passing down style strings to elements won't always be enough to achieve the desired outcome. If we wanted to solve this propblem, we would have to expose implementation details in order to give the desired result.
- Finally, we lose flexibility in how we apply the options. We may want to use (or remove) certain tw classes for certain style arguments but we can't do that if we are just passing CSS down.
I'm also nto a huge fan of mapping gr.CSS classes to elements for the reasons we discussed above. I think a nested API is fine but that nesting shouldn't map to specific elements, but specific semantic concerns. For example a component might have a button, a label, and an 'input', conceptually, but passing values into those CSS objects should not map to individual elements. I don't think that really gives enough flexibility.
Also can you explain the API in more detail? I don't under what an array of gr.Styles would do.
| if border_width is not None: | ||
| style_dict["border-width"] = str(border_width) + "px" | ||
| if border_color is not None: | ||
| style_dict["border-color"] = border_color | ||
| if rounded: | ||
| style_dict["border-radius"] = "0.5rem" | ||
| if background_color is not None: | ||
| style_dict["background-color"] = background_color | ||
| if text_color is not None: | ||
| style_dict["color"] = text_color | ||
| if text_size is not None: | ||
| style_dict["font-size"] = str(text_size) + "px" | ||
| if bold: | ||
| style_dict["font-weight"] = "bold" | ||
| if visible == False: | ||
| style_dict["display"] = "bold" | ||
| if height is not None: | ||
| style_dict["height"] = "100%" if height is -1 else str(height) + "px" | ||
| if width is not None: | ||
| style_dict["width"] = "100%" if width is -1 else str(width) + "px" | ||
| style_dict.update(kwargs) | ||
| return style_dict |
There was a problem hiding this comment.
I don't think we should do these conversions here, it gives us less flexibility on the frontend in how we actually process the styles. We would find it difficult to add tailwind classes in resposne to some style arguments, for example. We don't want to be passing all css properties down to the elements. I would just send the kawrg contents as is, so we can decide what do with it. In some cases that will be a style attribute but it might not always be.
| <div class="mt-6 p-2"> | ||
| {#if recording} | ||
| <button class="gr-button !bg-red-500/10" on:click={stop}> | ||
| <button class="gr-button !bg-red-500/10" style={style["record_btn"]} on:click={stop}> |
There was a problem hiding this comment.
Similar to the comments above, we don't want to just pass everything down to the element. We want an explicit interface like label_bg etc. It would be really easy to break layouts with arbitrary CSS. I would just make the record available to each component and let it decide how the CSS is handled.
Keeping this interface constrained and gradually opening it up makes more sense to me in terms of ensuring gradio Apps always look good. Theming should be where we really let people customise the general look and feel (when we figure out how we'll be doing that).
|
I think |
This PR allows CSS control both at a page level and a per-component level. For the page level, the
css=kwarg has been restored, which takes an arbitrary string of CSS, along withelem_idComponent attribute that adds an element id to any component. For the Component level, I've introduced a gr.style function that will allow us to provide controlled styling control that also will allow targeting specific nested components.Take a look at the example code below (from demo/blocks_simple_squares/run.py):
which renders