-
Notifications
You must be signed in to change notification settings - Fork 553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Contextual Overrides / Theming Proposal #147
Comments
It took me a few reads to understand what you're suggesting. I think i understand it now, and I think this is a solid idea, though I also liked the idea you deemed too dangerous:
This can become quite dangerous indeed, but is also the easy quick-fix people are looking for. This felt like a really good idea, to me, the first time I read through this proposal. It's up to the developer to use this with care, and only override sub-components the main-component composes. This solution does benefit from not requiring an additional file.. I really like your proposal! May I propose re-using naming already familiar to CSS modules for your "confusing naming" problem?:
|
@geelen thanks for writing this! I don't understand how A How can The ability to import a class name still sounds the best to me: // Item.css
.normal { background-color: gray; }
.important { background-color: red; } // List.css
.normal :external(important from './Item.css') {
background-color: pink;
} |
See css-modules/css-modules#147 for API discussion, I'm just aping the proposal for now to see if it feels good.
If you're writing a component that uses CSS modules and want it to be styleable, wouldn't you accept the classname hash as a param? Then client code that wants to override your styles can pass a different classname hash to get their own styles. |
See css-modules/css-modules#147 for API discussion, I'm just aping the proposal to see if it feels good.
See css-modules/css-modules#147 for API discussion, I'm just aping the proposal to see if it feels good.
Sorry for the reference spam, but tivac/modular-css#inherits has a working implementation of this proposal available in browserify/rollup/API/etc flavors. Hoping to play with it after the US holiday weekend and be able to provide some more concrete feedback! 👌 |
I'm with @ndelangen all I want is this syntax: I find it endlessly frustrating to not have a simple and straightforward way to say: "In this Header buttons are laid out like this" , and completely agree that layout should be a product of a component for it's children not a component for itself. generic |
I'm warming to this simpler syntax but only as an "escape hatch". As in, not the only way to do theming, but a way to get out of a bind, that's better in some cases than Something that just occurred to me though, that's supported right now, is the idea of composing a nonce global class in the inner component: .button {
composes: button-override-hook from global;
/* more styling here */
} This adds the class .header { /* styles */ }
.header .button-override-hook {
/* contextual overrides */
} Maybe another name would be better: |
@geelen Isn't that just basicly this:
maybe I'm misunderstanding what your code example
Although this is a way to solve the problem, it's fragile, and adds a lot of boilerplate in the the child component. |
The issue with your Logically, if I have a To put it another way, any class that composes
but it's worse than just having something like explicit exports in that it's more cumbersome to use, and is less modular because it involves declaring everything in that hypothetical So given all that, I think it makes more sense for that |
Oh, sorry, I misread – the global class would work, but you lose all the advantages of namespacing, &c. that using CSS modules gives you. So given that, it seems like the most minimal way would be to declare specific exported hooks in CSS modules that can be reached into from elsewhere, rather than e.g. make all classes accessible via |
Though my magical Christmasland wish is something where, if you do export a selector, you restrict the set of properties that can be overridden. I'm not sure that's possible to do in any reasonable way, though. See e.g. https://gist.github.com/taion/ac60a7e54fb02e000b3a39fd3bb1e944 for an intentionally embarrassingly crude illustration. |
Vote for |
I'm really looking forward this |
This is a really interesting proposal to the issue I am having atm. My Use CasePersonally I have an app that is used by multiple clients who should all be able to apply theming to the entire app but the general structure remains the same. The app consists of x number of modules that is compiled into one JS and one CSS. The goal for me is not to have x number of versions of the JS nor the CSS but to just add a theme style that adjust some styles here and there in order to; 1. leverage caching in the browser, 2. Make the theming totally optional. So basically I would have files like this:
Each of the So I am searching for a way to have a list of the used classes so I might override them and I really think that the My humble suggestionPersonally I like the
Would produce:
A bit like the ES6 module inheritance but where you just import the class names and build from that. What would also be neatWhat I would like as well is to be able to build a theme CSS that just overrides the default properties by changing the values in some variables. So basically it would take the selectors from where the variables are used and create a block where the properties are overridden with the new value. The idea it to keep the simple structure of the override CSS mentioned above, but creating it dynamically without having to know where and how the variables are actually used in the app. But then again that might be out of the scope of CSS modules and possibly a job for something like POST CSS. |
I added the |
I have a proposal for theming external library. The traditional approach to customize vendor is to import our external dependencies and rebuild them with our sass variable a good example is bootstrap sass. The disadvantage of this method is you need to build your external dependencies and you need to build them with a certain type of tool. It's something in Js does not happen because node_modules are already built but you can still configure what you are using. The following code examples I try to explain how we could use custom properties to create real css module and sharing css values to js. app.js import { h, render, Component } from 'preact';
import { Btn } from 'node_modules/vendor.js';
console.log(Btn.style.button); // => "button-hash-123"
console.log(Btn.style._customProps.color); // => "--color-hash-456"
console.log(Btn.style._values.abstractedVariable); // => "red" app.css @value Btn from "node_modules/vendor.js";
@value vendorColor from Btn.style._customProps.color;
@value vendorAbstractedVariable from Btn.style._values.abstractedVariable;
:root {
--color: red;
vendorColor: green;
--new-theming: vendorAbstractedVariable;
} node_modules/vendor/index.js export Btn from './btn/index.js'; node_modules/vendor/btn/index.js import { h, render, Component } from 'preact';
import style from style.css
class Button extends Component {
render() {
return <button type="button" class={style.button}>click</button>;
}
};
export { Button, style } node_modules/vendor/btn/style.css @value abstractedVariable: red;
@value screenMedium: (min-wdth: 700px);
.button {
color: var(--color, red);
width: 100%;
@media screenMedium {
width: auto;
}
} output: {
"button" : "button-hash-123",
"__customProps" : {
"color" : "--color-hash-456"
},
"_values" : {
"abstractedVariable": "red",
"screenMedium": "(min-wdth: 700px)"
}
} output: vendor.css .button-hash-123 {
color: var(--color-hash-456, red);
} |
I have some simple idea about external realization @value child from './child.css';
.parent .child:hover {
background: blue;
}
or
.parent :external(child from './child.css'):hover {
background: blue;
} What if values will also replaces selectors? /cc @michael-ciniawsky @geelen @sullenor @sokra |
With I'm not sure if literally |
I've been noodling about this for ages. In most programming environments, when I want to make something extensible / customizable / whatever, I offer parameters, callbacks, etc. Could css-modules have parameters? For instance:
Importing such a css-module to JS would return a function rather than an object; call it with params to get the resulting styles. |
@DanielHeath The different is the same as commonjs and es-modules. css-modules are static. You want something in runtime. But there's css custom properties which introduces this runtime natively. |
Yo, not yet. But icss spec said it should. |
Prevent localizing we can by checking existing |
I just mean the lexicographic difference between |
Exported classes can be imported by value. |
@TrySound Just following up here – with this proposal, is there any way to only have specific selectors be |
If we had a function that merges classes of two (or more) css module objects, one could simply merge the default styles of a component with the ones that are passed in with // Shared Component.
import {mergeClasses} from "css-module-helpers";
import defaultClasses from "./default.css";
const Headline = (props) => {
const classes = mergeClasses(defaultClasses, props.classes);
return (
<div className={classes.wrap} > // outputs: <div class='defaultWrapHash customWrapHash'>
<h1>
{props.title}
</h1>
</div>
);
}; The component consumer can apply her custom styles simply like this: import Headline from 'headline-package'
import myCss from "./custom.css";
<Headline classes={myCss} title="Hello"/> I think this approach would also work for deeply nested components, because if I build a component and use a third party component in it, I would not choose class-names that are already in use by that third party component, so each identifier remains unique. And this will go up the chain, since the person who builds her component based on my component wouldn't choose the same names either and so on and so on. And if a parent accidentally comes up with a class-name that is already in use by one of the (imported) children, I would consider it a bug in the component, which can easily be fixed. To make sure that your component doesn't use the same class-name as one of its children in the chain, the compiler could throw a warning (if that is possible). |
@feluxe I implemented your idea here: https://github.com/mrac/decoupled-styling-css-modules The approach is similar to https://github.com/javivelasco/react-css-themr#the-approach but using CSS modules. Instead of having dangerous access directly to real class-names in external files, we just reference the human-readable class names defined in nested components CSS. I used Example of form component with button child:
|
Is there any update on this? |
There is a css-modules-theme that does theme objects composition fast, and result is cacheable and shareable between components. |
Great reply, thanks a lot! This should be resolved once and for all, it's been too long (I'm just a madman, don't mind me 😄). |
Playing around with css-modules-theme solved some of the before-mentioned issues, but it some common cases it has significant limitations.
for example This common case could be easily solved by using the I'd like to hear further suggestions/opinions about this. |
@shiro Right, in your example with Buttons inside Header, for instance <Header>
<Button/>
<div><Button/></div>
<div><Button/></div>
</Header> Header can't clone nested buttons to pass theme, it's not feasible approach. Plus, what if third Button should have special theme, that Header should not override? So example can be solved in two ways.
import styles from './Component.css';
import stylesHeader from '../Header/Header.css';
return (
<Header>
<Button theme={stylesHeader}/>
<div><Button theme={stylesHeader}/></div>
<div><Button theme={styles} themePrefix="head-"/></div>
</Header>
);
// HeaderButton.js
import styles from './Header.css';
import {mixThemeWithProps} from '@css-modules-theme/react';
return <Button {...mixThemeWithProps(styles, this.props)}>; import styles from './Component.css';
return(
<Header>
<HeaderButton/>
<div><HeaderButton/></div>
<div><Button theme={styles} themePrefix="head-"/></div>
</Header>
); In the last case you can even additionally style HeaderButton from parent if you need too, |
In css-modules-theme 2.x it is possible to do multiple themes composition. In case you want to compose theme from props and context (in case of react) |
I'd like to round off this issue, by saying that after fixing a bug with css-module utils, this is now possible, and supported in at css-loader; .button {
color: blue
} @value button from './button.css';
.toolbar > .button {
color: red
} |
@jquense Great news! Can you link the associated PRs? |
I will add it's still sort of a hacky workaround for proper support, it doesn't handle cases where classes have been composed in the way a proper ':external' does a la modular-css |
Thanks @jquense! any follow up on the PR link? Another great thing is that renaming is possible, for example:
works! Adding some error output would be very usefull, since there's no error or message being emmited if:
Finally I'd like to know if there's a shorthand for importing many values from the same file, rather than having to write 10 lines to import 10 values, since this gets messy quick. Is the API stable and documented somewhere, can I use this in production? Thanks for the amazing work on this! |
I have had the same problem working with CSS Module in React. I had been searching on the internet but I haven't found any easy way of overriding the style of the components. For that reason, I decided to create a webpack loader called group-style-loader. This loader uses the approach of pass the style through a style property on the components. The working of this loader is simple, you can declare the CSS classes normally as you are used to, but you can also use a special separator to group the classes in a nested style object.
This is useful when we want override the style of other components. For example, if we have a Card component and we want to override the style of that component, we only need to pass the style through the
I have used this loader in one of my own projects, and I could reduce the base code of the project by 200 lines. An advantage of this loader is that you do not need to use If you want to know more about how to use this loader you can visit its repository group-style-loader or you can install it from NPM. I hope that this loader can serve as a workaround while found other solutions. Please, feel free to ask me whatever question. |
Is this proposal due to land any time soon, or what is the outcome currently? |
Contextual Overrides / Theming Proposal
There have been a series of discussions on this issue tracker and on other projects about the interrelated issues of Contextual Overrides (i.e. how can an outer component change the CSS of an inner one?) and Theming (i.e. how to publish a reusable component & let the consumer style it to fit with their app?). There have been several promising proposals about how to deal with this, so I want to take an overarching look at the problem and propose a solution.
Contextual Overrides
There's a great discussion dating back to August last year at #40 which presents the use-case of overriding a the width of a child component
.card
inside a parent component.row
and a bunch of potential solutions. There's an earlier one #21 that tries to reconcile something like Bootstrap's.buttonGroup
changing the appearance of a.button
and how that might be possible in CSS Modules. More recently, there's been a discussion in https://github.com/gajus/react-css-modules/issues/94 leading to proposal #139 to allow an explicit method of importing a compiled class from another file in order to change it. There's a lot of good points being made and it's worth reading through the use cases. There's also a non-CSS-modules summary of approaches by Simuari that's well worth reading.The prevailing wisdom is that this kind of styling breaks encapsulation, and therefore goes against the principles of CSS Modules, which is true, if a little unsatisfying. And there are several workarounds proposed, such as:
.buttonGroup > .button
, a button inside a group becomes<Button variant="within-group"/>
or<GroupedButton>
. This keeps the styles contained with the component and adds the information to the public API through the "variant" property. This is arguably the most widely-applicable approach, and most people seem to be finding it's good enough.So let's ignore Option C for now, and look at some of the issues raised by adopting A & B.
Variant fatigue
I've come to the conclusion that treating every possible styling change as a variant of a component works only up to a point. For a button you have legitimate (i.e. semantic) variants like "primary", "danger", "inactive" but often you might find yourself adding presentational ones like "small", "medium," "large" or contextual things like "on-header" or "has-icon" or "inline". If they form logical groups like this you can add them as attributes (
<Button variant="danger" size="small" icon="warning">
) which ends up being pretty ok.Where this breaks down is where you add a variant for the sake of the one place that needs a particular thing (i.e. "on-header" or "inline" from before). This leads to _non-semantic variants_ purely for the sake of trying to maintain encapsulation. Which, like making a private method public so you can test it directly, signals that you've modelled something wrong. Don't get me wrong, sometimes the "right" way isn't at all obvious and taking a little shortcut to maintain 100% test coverage or avoid writing descendant selectors is totally worth it.
You can also make the argument that you should never need non-semantic variants — that you should adapt the designs to maintain consistency. I encourage you to push this argument as far as you can for your project — visual inconsistencies are usually a gradual development and a healthy UI build can identify and address them when they appear. But there's always a point where refactoring the design isn't possible and shortcuts have to be taken. But in that case I still want the system to work! I don't want the first shortcut taken to spell the end of good CSS engineering on a project.
In that vein, I think there's a quick win for avoiding non-semantic variants.
Layout is special
In the push to draw component boundaries around all our UI elements, it's natural to assume that CSS properties like width, height, flex-basis, float, and margin are all part of the component. But one of the main use of contextual overrides is to change some assumption about a component's surroundings. The discussions linked to above have multiple examples of this, and in fact the whole Option B of adding an extra layer of divs is precisely to deal with layout issues. But what if components didn't describe their own layout?
The whole concept of a CSS Grid is to isolate layout from presentation, because it's not possible to manage a float-based layout component-by-component. But with flexbox being the boss monster it is, and the Grid spec around the corner, it's tempting to make the component contain its own layout information. But those are the first things you end up needing to override.
A better approach is to treat layout as special and remove it from the component. This doesn't necessarily mean you should build a
<Row>
or<Column>
component (though that does work) but rather adopting the following rule: A component lays out its children, not itselfFor example:
By using the
> *
and the> * + *
selectors we have control over how our children are laid out without caring what those children are. It provides contextual layout information without coupling. If we have one child, it will fill the whole space. If two or more, they will subdivide the space and be separated by a consistent margin.Or, if we were building something like the AirBnB header with one element left-aligned and the rest right-aligned (but with consistent spacing between):![image](https://cloud.githubusercontent.com/assets/23264/14940864/4e5df666-0f87-11e6-8d0d-1d8896b0a951.png)
Now our
<Logo>
or<HeaderNav>
components don't care about where they're drawn, they just draw. The<Header>
component is the one that cares about the layout, but it cares about its internal layout, not external.In effect, we've replaced the need for Option B (an intermediate layer) with generic, direct descendant classes. But either way (extra divs or
> *
selectors) making layout the job of the parent is a big win.So that's all well and good for your own sites, where you can ensure each component doesn't assume too much about its external layout, and you can jump into each component to add a new variant if you need one, but that doesn't help that much if you've just pulled some code from NPM.
Theming
What about publishing the next Bootstrap? Or an individual, reusable component? How can you provide visual consistency but also flexibility so each instance doesn't look identical?
This is a wider discussion and I'm by no means an expert. But there are a few discussions I've been following, most closely elementalui/elemental#53 which touches on a lot of the issues for framework maintainers & the variety of their users' needs. However the entire Web Components movement has these same challenges, and I'm less than well-versed on those. Please feel free to comment if you have a valuable perspective on this issue.
I'm going to use the word "theming" to cover a variety of use-cases & everything in between:
settings.scss
files (which are thousands of lines long, but we'll come back to that)..FormGroup > input + i { display: none; }
.I think we need tooling & conventions for building reusable components that are themable in all these ways, since it's hard to anticipate the ways your users might want to use your component. You might anticipate a user only wanting to tweak a few settings, but someone else will want to use it completely differently.
Theoretically, a user might want to change the markup that a component generates. This crosses a line that I do not consider it "theming" any more — I think markup & behaviour are intrinsic to the published component whereas its CSS is not. Therefore in this case the only solution is to fork the component and optionally PR the changes upstream (potentially as a new, optional variant of that component).
Of course, if we could figure out a way to build components to be themed in unexpected ways, we would also have a mechanism for avoiding non-semantic variants or global CSS overrides in our own code.
Replace everything (Theming #3)
In the worst case, none of the components styles are to be kept. We need to supply styles for every element inside the component, so we need a way to target them.
A given component might have several elements with styles targeting them directly as well as being involved in complex selectors such as
:hover
. Then there may be contextual classes like.-has-validation-error
or variant classes likedanger
. And each component will be different — one may usecomposes
to export multiple classes for a single element whereas one might attach multiple different classes to an element in JS. The only common point of truth is the exports of the CSS Module. No matter what style the component is written in, at some point it needs to look up the class names on thestyles
object. This is where we can hook in.react-themable already does this, though I think we can simplify by only supporting classes, never inline styles — converting a set of inline styles into a one-off class is pretty straightforward in most cases, but you can't go the other way beyond the simplest of rules.
So if we treat the set of _classname keys_ that a component uses as part of its public API, we can provide a wholly new stylesheet without changing the components behaviour. Relatively easy!
Sadly, replacing everything isn't the most common use case. Let's look at the other extreme.
Tweak one thing (Theming #1)
Let's say we wanted to override a single line of CSS in a component. This is often the sort of thing we would use a contextual override for in our own code, but our usual workaround of adding a variant no longer applies (since we can't easily edit the source code).
Some have suggested explicitly allowing an outer module to reference the compiled classname of an inner component, something like:
This would in fact let us override anything we wanted from the outside in, both for our own components and ones we've imported, but again it breaks incapsulation. The use of CSS descendant selector (not direct descendant) means this rule could have far-reaching consequences, something CSS Modules is designed to avoid. But I think the real problem is how easy it is to have a truly global effect:
Just by including this CSS file in your project you're adding a side-effect to every instance of
.inner
, which is really bad. So I don't see how this approach can work without serious restrictions on its usage.Adding, not replacing classes?
A new project, react-css-themr, has sprung up along these very lines, and is worth a look. But I think we should be aiming for something core to CSS Modules, that can work with more than just React.
There's a suggestion there that instead of replacing the classes that get used in the component the original classes might simply be extended with customising classes. This has some nice properties, since it's assumed that most of the time you're only going to want to be modifying styles, not replacing them wholesale. But I don't think this is the place for this option.
Firstly, you might want to simply extend one class but completely override another in the same file. Surely a flat rule where any passed in class overrides the default classes is simpler. Besides, CSS Modules already has a mechanism for exporting a class as a list of others plus some extra stuff, composition.
Allow partial specification?
If a component defined three classes
a
b
andc
and you only wanted to overridec
, you don't want to have to write this css:If the component changed and
d
was added, you'd have to update every call site or else you'd have broken styles. So it might be tempting to suggest that the passed-intheme
object and the defaultstyles
object just get merged (usingObject.assign
), but again I think we can do better.For one, it relies on every component to follow that same merging logic, instead of having a hard & fast rule of "If you're provided a
theme
object, use it, otherwise use your own styles". Let's keep the API simple and put the cleverness in the tooling.Secondly, it means that you can't partially override anything but the default styles. Why build something to handle a specific case if you can build a general one?
Proposal: Module Inheritance
So here's where I've landed — this is the best way I think we can support the use-cases I've outlined.
This
@inherit
rule simply passes through to ICSS, the composedsuper
gets exported like normal:The
@inherit
keyword then needs to be handled specially by the loader, going off and fetching "./base.css" as normal. But, when returning the object to JS land, instead of being a normal object, it's instead a call toObject.create
to set up a prototype chain:The inheritance chain makes the resulting object appear like it has everything, but as each compiled classname is pulled out, it's either being retrieved from the child module or the parent. Prototypal inheritance really does feel like it suits.
Pros
Explicitness & conciseness. You specify only what you mean to change, as well as where to fill in the blanks. Without context-switching from CSS to JS. Without writing anything redundant. And it works whether you're overriding just one class or almost all of them.
Components have the simplest possible contract. They use a mapping from keys to class names, which they either supply themselves or use an object passed in. There are no constraints on the types of keys you can use either, but you should consider the set of keys part of your public API (and respect semver accordingly).
For your own components, follow the same pattern. Then you can push non-semantic variants to their call site (i.e. make
<GroupedButton>
simply inherit from<Button>
but with your overrides applied)A module can still be evaluated in isolation. In CSS Modules, the list of keys in an
:export
block are known ahead of time. If we decided to include a syntax like@value * from "component/default.css"
we would need to resolve imports before we could replace values in the file, which might be pretty surprising. The point of an@inherit
statement is to say "Everything I don't specify, you'll find in here" which I think is simple enough to track things down but powerful enough to not slow you down.Trusty console.log shows good output. If you're debugging, you can see what symbols you've exported and which are coming from an inherited import:
Cons
Confusing naming. I'm not sold on
@import
orsuper
as names, maybeprototype
orparent
could work for both. Maybe you don't needsuper
, since you could just.b { composes: b from "component/default.css"; }
anyway. Happy to hear suggestions.Requires changes to all loaders. But then again, we've had lots of requests to deal with this and I can't see a better way without building something in to CSS Modules core, so I think we need to do something.
Requires an extra file. If you want to change one line of CSS about a component, you still need to make a new module file that inherits it, rather than using a descendant selector or something. But I think this is beneficial, since "one override" can turn in to many, and descendants get messy. It gives you pause to think whether there might be a better way to structure things, too (by moving layout to the parent, perhaps)
There's probably more I could write, but I've been trying to finish this post off for A LONG TIME so I might leave it at that. Hope this makes sense, let me know what you think!
The text was updated successfully, but these errors were encountered: