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

Open
geelen opened this Issue May 5, 2016 · 36 comments

Comments

Projects
None yet
@geelen
Member

geelen commented May 5, 2016

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:

  • A. Make more components, or treat the contextual overrides as different variants of a component. So instead of a CSS rule being .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.
  • B. Add an extra layer between the parent and the child. This only works well for layout properties, but does handle some of the use cases presented for contextual overrides. In fact, treating layout as a property of the parent changes the set of cases we might want contextual overrides for.
  • C. Use global classes or tag selectors. This is still CSS, we can hack & slash our way through and get the result we want. But the problems with this are the same as global CSS, which is what CSS Modules was invented to fix. Even using direct descendant selectors to stop our rules from cascading down the DOM means we're coupled to the internal structure of the child component. So, while we can always use this an escape hatch, we need to build the rest of the ship first.

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 itself

For example:

.Header {
  display: flex;
  /* presentational css here */
}
.Header > * {
  flex-grow: 1;
  flex-shrink: 0;
}
.Header > * + * {
  margin-left: 1rem;
}  

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

.Header > * + * {
  margin-left: 1rem;
}
.Header > :nth-child(2) {
  margin-left: auto;
}

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:

  1. Tweaking variables such as simply changing a few accent colours. The component or set of components are all mostly unchanged except for pre-determined settings. This is what Bootstrap & Foundation allow with their Sass builds and their settings.scss files (which are thousands of lines long, but we'll come back to that).
  2. Wanting to fairly significantly change some sub-component, for example hiding a label or icon that the component author has assumed will always be present. This sort of thing isn't well captured by a list of settings, and usually results in deep selectors like .FormGroup > input + i { display: none; }.
  3. Wanting to keep the behaviour of a shared component but supply completely new CSS. If a component has lots of existing CSS that's not driven by a settings file, it can often be easier to copy-paste the original file and rewrite whole sections, rather than try to override them from the outside.

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 like danger. And each component will be different — one may use composes 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 the styles 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:

.outer :external(inner from './elsewhere.css') {
  /* override here */
}

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:

:external(inner from './elsewhere.css') {}
body :external(inner from './elsewhere.css') {}
:global(:root) :external(inner from './elsewhere.css') {}

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 and c and you only wanted to override c, you don't want to have to write this css:

.a { 
  composes a from "component/default.css";
}
.b { 
  composes b from "component/default.css";
  /* extension */
}
.c {
  /* override */
}

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-in theme object and the default styles object just get merged (using Object.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.

@inherit "component/default.css";

.b {
  composes: b from super;
  /* extension */
}
.c {
  /* override */
}

This @inherit rule simply passes through to ICSS, the composed super gets exported like normal:

@inherit "component/default.css";

:export {
  b: b__31531abcfea super;
  c: c__bacdfe35631;
}
.b__31531abcfea {
  /* extension */
}
.c__bacdfe35631 {
  /* override */
}

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 to Object.create to set up a prototype chain:

// something like this is generated by the loader
import parent from 'component/default.css';
export default Object.create(parent, {
  b: { value: parent.b + " b__31531abcfea"; },
  c: { value: "c__bacdfe35631" }
})

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:

image

Cons

Confusing naming. I'm not sold on @import or super as names, maybe prototype or parent could work for both. Maybe you don't need super, 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!

@ndelangen

This comment has been minimized.

ndelangen commented May 6, 2016

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:

.outer :external(inner from './elsewhere.css') {
  /* override here */
}

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?:

@composes parent from "component/default.css";

.b {
  composes: b from parent;
  /* extension */
}
.c {
  /* override */
}
@elado

This comment has been minimized.

elado commented May 9, 2016

@geelen thanks for writing this!

I don't understand how @inherit works in this situation:

A <List> that renders <Item>s. Item has a few variants.

How can <List> affect how certain <Item>s are styled? It shouldn't just @inherit Item.css as it may also contain other types of items, which are different components.

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;
}

tivac added a commit to tivac/modular-css that referenced this issue May 24, 2016

Starting in on inheritance prototype
See css-modules/css-modules#147 for API
discussion, I'm just aping the proposal for now to see if it feels good.
@DanielHeath

This comment has been minimized.

DanielHeath commented May 25, 2016

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.

tivac added a commit to tivac/modular-css that referenced this issue May 27, 2016

CSS Modules inspired inheritance prototype
See css-modules/css-modules#147 for API
discussion, I'm just aping the proposal to see if it feels good.

tivac added a commit to tivac/modular-css that referenced this issue May 27, 2016

CSS Modules inspired inheritance prototype
See css-modules/css-modules#147 for API
discussion, I'm just aping the proposal to see if it feels good.
@tivac

This comment has been minimized.

tivac commented May 27, 2016

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! 👌

@jquense

This comment has been minimized.

jquense commented Jun 8, 2016

I'm with @ndelangen all I want is this syntax: .outer :external(inner from './elsewhere.css')

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 > * selectors just don't cut it, are super fragile and don't provide any sort of encapsulation or safety over being able to reference a the class you want to affect.

@geelen

This comment has been minimized.

Member

geelen commented Jul 20, 2016

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 .outer div or .outer > *

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 button-override-hook onto any element that uses the button styles, just like normal composition. But unlike proper composition from another module, the compiler doesn't care if that class isn't used anywhere. You can then override it like so:

.header { /* styles */ }
.header .button-override-hook {
  /* contextual overrides */
}

Maybe another name would be better: __button or hooks--button or something. Might be useful?

@ndelangen

This comment has been minimized.

ndelangen commented Jul 20, 2016

@geelen Isn't that just basicly this:

C. Use global classes or tag selectors. This is still CSS, we can hack & slash our way through and get the result we want. But the problems with this are the same as global CSS, which is what CSS Modules was invented to fix. Even using direct descendant selectors to stop our rules from cascading down the DOM means we're coupled to the internal structure of the child component. So, while we can always use this an escape hatch, we need to build the rest of the ship first.

maybe I'm misunderstanding what your code example composes: button-override-hook from global; does, this is what I'm assuming you mean to do:

/* child.css */
:global(.child-override-hook) {
}
.child {
  /* base child styling */
  composes: button-override-hook;
}

/* parent.css */
.container :global(.child-override-hook) {
  /* overrides child styling */
}

Although this is a way to solve the problem, it's fragile, and adds a lot of boilerplate in the the child component.

@taion

This comment has been minimized.

taion commented Jul 25, 2016

@geelen

The issue with your .button-override-hook proposal above is that it's essentially the same as :external(inner from './elsewhere.css') in practice, but with worse ergonomics.

Logically, if I have a Button.css, the place where it would make the most sense to declare an "external-able" hook would be in that Button.css, not in some separate, top-level global.css.

To put it another way, any class that composes button-override-hook from global has all the issues you mention in

:external(inner from './elsewhere.css') {}
body :external(inner from './elsewhere.css') {}
:global(:root) :external(inner from './elsewhere.css') {}

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 global, rather than declaring a single specific exported hook in e.g. Button.css.

So given all that, I think it makes more sense for that .button-override-hook to be some specifically declared export from Button.css, rather than something with worse encapsulation by going to a top-level global.css.

@taion

This comment has been minimized.

taion commented Jul 25, 2016

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 :external.

@taion

This comment has been minimized.

taion commented Jul 31, 2016

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.

@klimashkin

This comment has been minimized.

klimashkin commented Aug 24, 2016

Vote for :external.
You can call it :dangerouslyExternal, but it's really needed.

@yachaka

This comment has been minimized.

yachaka commented Aug 24, 2016

I'm really looking forward this :external selector too.

@Tokimon

This comment has been minimized.

Tokimon commented Sep 27, 2016

This is a really interesting proposal to the issue I am having atm.

My Use Case

Personally 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:

// Main application build
app.js

// Main CSS build from the usage in the JS
app.css

// Client CSS files (each file overrides stuff from app.css)
client1.css
client2.css
client3.css
.. etc etc etc

Each of the clientX.css files shold ONLY include the overrides and nothing more, but since the class names are dynamic I am looking for a way to have a list of class names being used to be able to override them. I am not really interested in adding extra class names to the JS build as the script should just be build once and included without having to have a specific include script for each client just because of the styling (which would be the case with the mentioned React method).

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 @inherit or :external() could be what I am searching for.

My humble suggestion

Personally I like the @inherit method a bit better as you include the file you are extending once and then do your overrides, but maybe it could be build upon a bit so you could override style form multiple files like:

@inherit "app.css" as app;
@inherit "customized.css" as custom;
@inherit "antoherStyle.css" as another;

:extend(app.title, custom.specialTitle) {
  color: red;
  position: fixed;
}

:extend(app.text, another.boxBody) {
  background: smoke;
  overflow: hidden;
  border: 10px solid red;
}

Would produce:

.app_title_aa12df3, .custom_special-title_1e3dd4a {
  color: red;
  position: fixed;
}

.app_text_dfaa129, .another_box-body_9edd25a {
  background: smoke;
  overflow: hidden;
  border: 10px solid red;
}

A bit like the ES6 module inheritance but where you just import the class names and build from that.

What would also be neat

What 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.

@tivac

This comment has been minimized.

tivac commented Nov 17, 2016

I added the :external(<selector> from <file>) proposal to modular-css@0.29.0 & it seems to be working out well so far as an occasional escape hatch.

@tivac tivac referenced this issue Jan 23, 2017

Closed

Nested Components #207

@MatteoWebDesigner

This comment has been minimized.

MatteoWebDesigner commented Feb 23, 2017

I have a proposal for theming external library.
I think the future of theming is using css custom properties.

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:import { Btn } from 'node_modules/vendor.js';

{
    "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);
}
@TrySound

This comment has been minimized.

Member

TrySound commented May 31, 2017

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

@taion

This comment has been minimized.

taion commented May 31, 2017

With @value, I like the idea of having explicit exports, rather than allowing :external to target literally anything, which would defeat a lot of the modularization.

I'm not sure if literally @value is the best name, though – a class isn't the same kind of "value" that you'd otherwise use @value for, no?

@DanielHeath

This comment has been minimized.

DanielHeath commented May 31, 2017

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:

@param brand-color default '#4433FF;
@param component-overrides default './component-base.css';

.component-wrapper {
  background-color: brand-color;
  composes: component-wrapper from component-overrides(brand-color: brand-color);
}

Importing such a css-module to JS would return a function rather than an object; call it with params to get the resulting styles.

@TrySound

This comment has been minimized.

Member

TrySound commented Jun 1, 2017

@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.

@DanielHeath

This comment has been minimized.

DanielHeath commented Jun 1, 2017

Good point. However, this can still work statically if you add the limitation that parameterized modules cannot be loaded directly from JS.

Your styled component can then:

import defaultStyles from './styles-with-default-values.css';
MyStyledComponent.defaultProps.styles = defaultStyles;

The caller can then use

import overriddenStyles from './styles-with-overridden-values.css';
<MyStyledComponent styles={overriddenStyles} />

Each of these css files can then import the parameterized CSS, but your assets are still built statically.

@TrySound

This comment has been minimized.

Member

TrySound commented Jun 1, 2017

Well, CSS modules can do nothing with this. They are to low level.

@taion

This comment has been minimized.

taion commented Jun 1, 2017

@TrySound Thinking about this a bit more, I really like your idea of using something like @value. Do you think @value is the maximally clear name here, though?

@TrySound

This comment has been minimized.

Member

TrySound commented Jun 1, 2017

It's not just clear. It's existing implementation. Postcss modules values

@taion

This comment has been minimized.

taion commented Jun 1, 2017

@value isn't used for selector-like things, is it? In your example above, you do @value child, but have .child. I don't think anything similar happens when using @value in other cases.

@TrySound

This comment has been minimized.

Member

TrySound commented Jun 1, 2017

Yo, not yet. But icss spec said it should.

@TrySound

This comment has been minimized.

Member

TrySound commented Jun 1, 2017

Prevent localizing we can by checking existing :import

@taion

This comment has been minimized.

taion commented Jun 1, 2017

I just mean the lexicographic difference between child and .child. Could it be something like :value(child)?

@TrySound

This comment has been minimized.

Member

TrySound commented Jun 1, 2017

Exported classes can be imported by value.

@taion

This comment has been minimized.

taion commented Sep 22, 2017

@TrySound Just following up here – with this proposal, is there any way to only have specific selectors be @value-able from other files? It seems like it'd be suboptimal if every selector could be targeted from other modules.

@feluxe

This comment has been minimized.

feluxe commented Nov 24, 2017

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 props.classes.

// 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).

@mrac

This comment has been minimized.

mrac commented Jun 21, 2018

@feluxe I implemented your idea here: https://github.com/mrac/decoupled-styling-css-modules
As far as I tested, it works!

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 __ syntax to reference nested children. Then in React component there's a JS function to decompose it into nested JS object of classes.

Example of form component with button child:

/* button.css */

.root {
  background-color: white;
}
.text {
  color: black;
}
/* form.css */

.okButton__root {
  background-color: yellow;
}
.okButton__text {
  color: red;
}
@shiro

This comment has been minimized.

shiro commented Sep 3, 2018

Is there any update on this?
Correct me if I'm wrong but at the moment there does not seem to be any practical solution to this very common issue.
Personally I think :external would at least solve the problem, even if it has to be used with caution.
Do we all have to write our css in js now? 😿

@klimashkin

This comment has been minimized.

klimashkin commented Sep 4, 2018

There is a css-modules-theme that does theme objects composition fast, and result is cacheable and shareable between components.

@shiro

This comment has been minimized.

shiro commented Sep 4, 2018

Great reply, thanks a lot!
Looks quite promising actually, perhaps the package is still a bit immature though?
Is this the most efficient way to go about it or would handling this at compile-time be better?

This should be resolved once and for all, it's been too long (I'm just a madman, don't mind me 😄).

@shiro

This comment has been minimized.

shiro commented Sep 4, 2018

Playing around with css-modules-theme solved some of the before-mentioned issues, but it some common cases it has significant limitations.
To name a few I've found:

  • Sometimes a container component doesn't know what it's children are, so it cannot (without cloning) pass the theme object down the hierarchy
  • Sometimes a component doesn't know the depth at which an overridden child will appear, making it impossible to override (by using prefixes)
  • The overriden children may appear in different places/depth levels in the hierarchy, making the approach wet (not dry 😆)

for example
A Header component that just renders its children attempts to override all Buttons.

This common case could be easily solved by using the :external pseudo selector in Header's style.

I'd like to hear further suggestions/opinions about this.

@klimashkin

This comment has been minimized.

klimashkin commented Sep 4, 2018

@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?
Theming should not be automatic (implicit), in that case it would lead to the same nesting problem as we have in global css. It should be explicit - caller component, that renders all listed above, should inject themes.

So example can be solved in two ways.

  • You can import Header.css styles and set it as theme to desired buttons
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>
);
  • You can create HeaderButton component as a wrapper on top of Button (react is all about composition) that will set header theme inside.
// 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, <HeaderButton theme={someStyle}/>. It's very flexible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment