-
Notifications
You must be signed in to change notification settings - Fork 18
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
Usage for Themes #6
Comments
Hey, this is a great question. I've been thinking about this a lot, and chatting with @markdalgleish about it the other day, and had this tab open to reply ever since! Ah well, better late than never. Ok, for some context, I think custom properties (i.e. css variables) are ABSOLUTELY the solution. I thought I had mark convinced too, but then he throws shade on the idea the other day and so now I dunno. Anyway, here's why I think it's pro as. The problem with theming that I can see is the inversion of control that is required. You want to define a default set of styles with a bunch of placeholders that get filled in by the consumer. But there's no real mechanism for this — in Sass you have mixins which can work at the component level but not for a whole UI framework. So you end up cloning Bootstrap, hacking // First your theme variables
@import 'custom-variables';
// Then the reusable component
@import 'component/index'; With CSS Variables, this changes. So let's say you publish a component with the following raw CSS (forget CSS Modules for a second, pretend this is being served statically off a CDN): :root {
--color-primary: blue;
--bg-primary: white;
--color-alt: darkblue;
--bg-alt: #CCC;
--color-inverse: white;
--bg-inverse: black;
}
.RadTable td {
margin: 0;
padding: 0.25rem 0.5rem;
}
.RadTable > thead, .RadTable > tfoot {
color: var(--color-inverse);
background: var(--bg-inverse);
}
.RadTable > tr {
color: var(--color-primary);
background: var(--bg-primary);
}
.RadTable > tr:nth-child(2n) {
color: var(--color-alt);
background: var(--bg-alt);
} This HTML suddenly themes that component (note the order of includes): <html>
<head>
<link href="http://some.cdn/radtable.min.css">
<style>
:root {
font-size: 14px;
--color-primary: green;
--bg-primary: #CCF;
--color-alt: darkgreen;
--bg-alt: #AAF;
}
</style>
</head>
<body>
<table class="RadTable">
<thead><!-- ... --></thead>
<tr><!-- ... --></tr>
<!-- ... -->
<tr><!-- ... --></tr>
<tfoot><!-- ... --></tfoot>
</table>
</body>
</html> What's happening is the And the reason I'm pretty confident that this is going to work is that we already have two variables that work like this — So what does that mean for CSS Modules? Well, the global nature of the variables is a bit against the spirit of local modules, so we'd want to a) not define them against EDIT: all variable names here would be hashed so there's no global collisions and then exported just like class names & animation names. /* rad-table.css */
/* these would all get rewritten by CSS Modules to be globally safe
& exported, just like classes or animations */
.vars {
--1rem: 1rem;
--color-primary: blue;
--bg-primary: white;
--color-alt: darkblue;
--bg-alt: #CCC;
--color-inverse: white;
--bg-inverse: black;
}
.table {
/* Now using a RadTable will always add the `.vars` class and, since
the other components are nested, they'll inherit those values. But if
the structure was different we could compose vars multiple times
as needed. */
composes: vars;
}
.table td {
margin: 0;
padding: 0.25rem 0.5rem;
}
.head, .foot {
color: var(--color-inverse);
background: var(--bg-inverse);
}
.row {
color: var(--color-primary);
background: var(--bg-primary);
}
.row:nth-child(2n) {
color: var(--color-alt);
background: var(--bg-alt);
} /* theme.css */
/* import the table so we have access to the compiled variable names */
@value --1rem, --color-primary, --bg-primary, --color-alt, --bg-alt from "rad-table.css";
.theme {
--1rem: 14px;
--color-primary: green;
--bg-primary: #CCF;
--color-alt: darkgreen;
--bg-alt: #AAF;
} // Doesn't matter which order these are imported in, the @value
// in theme.css makes sure rad-table appears in the DOM above
// itself, so the cascade is dependable
import theme from "./theme.css";
import styles from "rad-table.css";
export default props => {
// You have to add the theme class to the table. Source order
// will mean .theme has priority over .vars
return <table className={styles.table + ' ' + theme.theme}>
<thead className={styles.head}>{/* ... */}</thead>
<tr className={styles.row}>{/* ... */}</tr>
{/* ... */}
<tr className={styles.row}>{/* ... */}</tr>
<tfoot className={styles.foot}>{/* ... */}</tfoot>
</table>
} To me, this is pretty neat. You can write a component with clear placeholders for styling that simply adding a class after the fact can hook in to. It really embraces the cascade in a way that might be harmful in global CSS, but I think it's a perfect fit for CSS Modules. Thoughts? |
You can safely use :root {
--color-primary: blue;
--bg-primary: white;
--color-alt: darkblue;
--bg-alt: #CCC;
--color-inverse: white;
--bg-inverse: black;
}
.table {
/* no need to compose the vars */
} because every Every themable component should have a /* theme.css */
.green {
--color-primary: green;
--bg-primary: #CCF;
--color-alt: darkgreen;
--bg-alt: #AAF;
} import { green } from "./theme.css";
<MyTable className={green} /> I would prefer omitting the @value color-primary, bg-primary from "rad-table.css"; Just like the |
Yeah I thought about using Your example would still need @value 1rem, color-primary, bg-primary, color-alt, bg-alt, table as base-table from "rad-table.css";
/* theme.css */
.table {
composes: base-table;
--color-primary: green;
--bg-primary: #CCF;
--color-alt: darkgreen;
--bg-alt: #AAF;
} import { head, row, foot } from "rad-table.css";
import { table } from "theme.css"; I quite like that. |
Here a more complete example: Note that
// table.js
import * as styles from "./table.css";
export class Table {
render() {
return <table className={this.props.className + " " + styles.table}>
{/* ... */}
</table>
}
} // app.js
import { Table } from "./table.js";
import { green } from "./themes.css";
class App {
render() {
return <Table className={green} />
}
} |
I'm thinking that variables could become another first class, local-by-default feature of CSS Modules. Values do a good job of allowing you to be explicit about variable dependencies between modules, but it certainly doesn't enforce it. I really like the guarantees that locally scoped classes and animations currently provide, and I think that variables are another candidate for this. I propose that the following: :root { --color-primary: blue; } ...would be compiled to this: :root { --HASH: blue; } Then if you want to override this variable, you explicitly need to import it. Importing and overriding a variable could look something like this, perhaps: @value --color-primary from './variables.css`;
.table { --color-primary: green; } Which, of course, is also compiled into this: .table { --HASH: green; } Thoughts? |
I looked a bit into custom properties, but did not really think about them in combination with css modules. Seems like their biggest downside, the global namespace, is a problem which can be solved as you show above. Still, they are not a technology available today and rather hard to polyfill, I imagine. Polymer seems to provide a subset of the functionality. But I couldn't find a project aiming for a stand-alone polyfill. A few more questions regarding the integration with css module values:
|
@markdalgleish Yeah that's exactly what I meant with my initial post. Sorry if it wasn't clear, but all variable names are local-by-default and hashed, hence why you have to use |
@geelen sorry—this is what happens when I try to catch up on a comparatively short train ride. At least we're on the same page 😄 |
Yeah just made an edit to clarify |
But it's cool, right? The way css variables which are suuuper greedy when global become just greedy enough when made local :) |
This is actually a really good example of how necessary values are, too. |
But are these really values? |
I like this idea! This is very much focused on theming from the top down. Because you're using the same hash for all variables of the same name (obviously so they can be re-assigned in a local selector if needed), what happens when you're setting the same var on root multiple times?
Is it just whichever is in the last-most dependency, or is there a way to tackle this? There's a decent chance someone could want components imported from y to have a different theme as those imported from x. |
I would disallow using imported vars in Declaration:root {
--xyz: 123px;
}
/* means: export xyz */ .className {
--xyz: 123px;
}
/* means: export xyz */ Overriding@value xyz from "./abc";
.className {
--xyz: 234px;
}
/* means: override xyz for this scope */ @value xyz from "./abc";
:root {
--xyz: 234px; /* error */
}
/* this is not allowed, because it has sideeffects (not locally scoped) */ Usage@value xyz from "./abc";
.className {
border-size: var(--xyz);
border-size: var(--xyz from "./abc");
} |
@tf yeah I think we'd have to carefully consider how to fit As for polyfills, they're basically not possible as far as I can tell. But support is improving! Chrome made some progress just last week, and Firefox support has been pretty steady. Anyway, it feels like something worth adding to CSS Modules in the future? That makes me pleased :) |
@sokra I'm not really sure this restriction buys us much. Given that there could always be another component using These side effects can becomes even worse when custom property values are used outside of the module that originally defined it like in your usage example. But I guess those are two sides of the same coin: Custom properties provide an interface to change components in a certain DOM subtree. And leaky rules are also possible with CSS modules right now, i.e. |
@geelen I think one has to be careful not to turn Maybe there could be something like |
Also |
Ohh I see @sokra's point. But the property names themselves get hashed, so maybe it's ok for them to have one predictable value site-wide, than to have the default value some places and the overridden one in others? @tf you're quite right, Also, |
It should only be possible to affect stuff depther in the DOM tree. The same is true for classes with CSS Modules. You can't import a class and modify it. You can't modify a value. When writing a component you can't affect other components, expect when they are nested inside your component. This rule must hold for custom properties with CSS Modules. You can't override vars in the |
Yeah. I see what you're saying. Should we stop people using
In fact, |
Also btw this is in Canary now under the "experimental web platform features" flag: https://twitter.com/addyosmani/status/661598908985049094 It's also started to land in Safari, which means Edge will have to follow suit: https://twitter.com/grorgwork/status/654747040644116480 |
Sounds good. This examples are fine: @value xyz from "./abc";
:global(:root) {
--xyz: 234px;
}
:global(a) {
--xyz: 234px;
}
.className > a {
--xyz: 234px;
} This examples are errors: @value xyz from "./abc";
:root {
--xyz: 234px; /* overwriting prev default value */
}
a { /* global tag without :global, overwriting var globally */
--xyz: 234px;
} In this examples :global :root {
--abc: 1px;
}
:root :global {
--abc: 1px;
}
.className :global {
--abc: 1px;
} |
With this coming in Safari 9.1 and iOS 9.3, and Chrome stable at end Feb, I think CSS vars are going to start becoming definitely usable. I like where we got to in this discussion, I'm gonna start thinking more deeply about this again. |
Have there been any examples of people using CSS Modules theming to date? |
Hey there! Even though that issue seems to approach its end of life... Maybe that's some help for you, @SpencerCDixon? |
Looks great @andywer I'll have to play around witih it my next project! Thanks :-) |
Support is looking pretty good for CSS variables now, anyone got any more thoughts on this now it's a bit easier to test? |
I wouldn't say support "is looking pretty good" when IE doesn't support custom properties at all. Also, isn't the point of theming to be global? |
I'd say so, especially in the context of this issue. Apart from IE/Edge all major browsers now support CSS Variables - certainly good enough support to begin looking into this in more detail and testing it across browsers. IE/Edge support will come. |
And it's going to be very difficult, if not impossible, to polyfill the non |
I'm a CSS Modules noob and I'm likely missing something but doesn't this tightly couple a module to the environment? Your CSS module now seems to need knowledge of the theme variables, files, etc.. That seems like it would make reusability difficult. Normally I would do something like @kevinSuttle has a very good point, I think, that themes are by nature, if not definition, global--they typically apply to an entire application or component of an application (meaning piece, not This is probably why every actual theme example I've seen passes an object into a Maybe one could transpile the Personally, I don't like the Locale and internationalization (i.e. right-to-left and left-to-right) seems like a very similar use case. |
@josh-endries in the current version of CSS modules this will work as you expect: :global(.mytheme) .button {} |
And to everyone else in the thread: how do you think about this issue 3 years later when CSS-variables adoption raised very high? There are css-in-js solutions now, like linaria that also generate hashes for css-variables names to avoid collisions. Unfortunately, these libraries are bound to React and Babel, so it would be nice to have a solution for vanilla CSS. |
I am really excited about this project. One thing I haven't wrapped my head around is how one could use these values to create "theme-able" components. Let's say there are some variables that allow customization of a component - things like colors and fonts. Since the component's css module has to import its values from some concrete file, I see no way how one could switch between two css files defining different values.
One could create two css modules that differ only in values, but then I would have to duplicate all the rules.
Basically, I wonder what would be the counterpart of the following SASS code:
Or is this simply something css module values are not supposed to solve?
The text was updated successfully, but these errors were encountered: