Skip to content

Instantly share code, notes, and snippets.

@geelen
Last active August 29, 2015 14:21
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save geelen/3255bf7b48abad32c68d to your computer and use it in GitHub Desktop.
Save geelen/3255bf7b48abad32c68d to your computer and use it in GitHub Desktop.
Traits

Global traits, local components

The idea is to combine the best bit of global styling (reuse, small payload) and local styling (total isolation, first-class React syntax)

This is combined with the concept of traits: you can think of them as permitted property/value pairs. Instead of every component being able to have every CSS property available to it, you can reduce your permitted set to X font families, Y font-size + line-height pairs, Z foreground/background colour pairs, W padding amounts. This is based off my work using amcss on real projects — traits were the single key feature that kept me using AM.

The one-sentence explanation: A site defines a set of permitted visual features, all components are simply a combination of those features

Definitions

@define-trait X establishes X as a type of trait. These include the above mentioned concepts: typography, colouring, spacing, layout, etc.

Any CSS inside a @define-trait :default block are shared by all users of that class. If a trait was button, this would be all the default styling for a button.

Any sub-rule under a trait indicates a trait variant. These get turned into classes, and components mix them in. E.g. the variant vertical of trait flex gets turned into the class .t-trait--vertical.

Usage

:local exports local to React, which is a set of classnames that include an auto-generated class-name for any one-off CSS if needed, plus any traits it includes. Using that in React is dead simple.

Inside a :local expression, a & block will allow arbitrary CSS to be written. That will be extracted into a one-off class.

Using traits inside a :local block should be the main way you include styles. The syntax is like pure css: trait: variant-a variant-b;.

@define-trait flex {
:default {
display: flex;
}
inline {
display: inline-flex;
}
vertical {
flex-direction: column;
}
wrap {
flex-wrap: wrap;
}
align-center {
align-items: center;
}
align-stretch {
align-items: stretch;
}
/* more flex-parent aliases */
}
@define-trait flex-child {
no-shrink {
flex-shrink: 0;
}
grow {
flex-grow: 1;
}
/* more flex-child aliases */
}
@define-trait colors {
default {
background-color: white;
color: hsl(240,7%,29%);
}
inverted {
background-color: hsl(240,7%,29%);
color: hsl(240,7%,99%);
}
recessed {
background-color: #eee;
color: black;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
/* more types of colour pairings */
}
/* more traits */
:outer {
flex: vertical align-stretch;
colors: default;
}
:header {
colors: inverted;
flex-child: no-shrink;
& {
height: 10vh;
min-height: 100px;
/* more one-off styles */
}
}
import Styles from './my_component.css!'
export default class MyComponent extends React.Component {
render() {
return <div className={Styles.outer}>
<header className={Styles.header}>
<!-- more header stuff -->
</header>
<!-- more stuff here too -->
</div>
}
}
/* from definitions.css */
.t-flex {
display: flex;
}
.t-flex--inline {
display: inline-flex;
}
.t-flex--vertical {
flex-direction: column;
}
.t-flex--wrap {
flex-wrap: wrap;
}
.t-flex--align-center {
align-items: center;
}
.t-flex--align-stretch {
align-items: stretch;
}
.t-flex-child {
}
.t-flex-child--no-shrink {
flex-shrink: 0;
}
.t-flex-child--grow {
flex-grow: 1;
}
.t-colors {
}
.t-colors--default {
background-color: white;
color: hsl(240,7%,29%);
}
.t-colors--inverted {
background-color: hsl(240,7%,29%);
color: hsl(240,7%,99%);
}
.t-colors--recessed {
background-color: #eee;
color: black;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
/* from my_component.css */
.my_component_18ab4f_head {
height: 10vh;
min-height: 100px;
}
<div class="t-flex t-flex--vertical t-flex--align-stretch">
<header class="t-colors t-colors--inverted t-flex-child--no-shrink my_component_18ab4f_head">
<!-- more header stuff -->
</header>
<!-- more stuff here too -->
</div>

Questions for you, esteemed internet friend

  • Is there a better prefix that t- for generated styles? Something better as a separator than --?
  • Is Webpack's local .local[className] syntax better than :className? I don't see much point in conflating the idea of class and export. :local means "this is a pseudo-selector, it will be compiled into something". .local[className] seems dumb to me...
@sokra
Copy link

sokra commented May 19, 2015

.local[className] was just an experiment. @markdalgleish proposed :local(className) or :local(.className). :className could conflict with other pseudo-selectors i. e. (:not or :root) and is not easy to distinguish with .className (:className)

/* without local scope */
.abc .def.ghi:not(.jkl) {}

/* with local scope */
.local[abc] .local[def].local[ghi]:not(.local[jkl]) {}
:abc :def:ghi:not(:jkl) {}
:local(abc) :local(def):local(ghi):not(:local(jkl)) {}
:local(.abc) :local(.def):local(.ghi):not(:local(.jkl)) {}
:local(.abc .def.ghi:not(.jkl)) {} /* my favorite */
@local { .abc .def.ghi:not(.jkl) {} }

Nice idea to mix multiple classes in the exported property. (traits)

This propably only work when selector is a single class, but still cool.


How about using ES6 like modules for CSS? This would make traits local too. (nothing need to be global)

@import { flex } from "./definitions.css"

definitions.css could just contain normal local-scoped classes. We shouldn't distinguish between local classes and traits. traits are just imported local classes.


/* definitions.css */
:local(.flex) {
  background: red;
}

:local(.flexDefault):inherit(.flex) {
  display: flex;  
}

:local(.flexInline):inherit(.flex) {
  display: inline-flex;
}
/* MyComponent.css */
@import { flexDefault } from "definitions.css";

:local(.outer):inherit(.flexDefault) {
  color: black;
}
/* from definitions.css */
.a1b2c3 {
  background: red;
}

.b2c3d4 {
  display: flex;  
}

.c3d4e5 {
  display: inline-flex;
}
/* definitions.css exports:
  flex: "a1b2c3"
  flexDefault: "a1b2c3 b2c3d4"
  flexInline: "a1b2c3 c3d4e5"
*/

/* from MyComponent.css */
.d4e5f6 {
  color: black;
}

/* MyComponent.css exports:
  outer: "a1b2c3 b2c3d4 d4e5f6"
*/

Traits would be a construct of a higher level language.


Optionally we can use this syntax if we want to be compatible to CSS:

/* MyComponent.css */
:local(.outer):inherit(.flexDefault from "definitions.css") {
  color: black;
}

Opinions?

@guybedford
Copy link

My opinion - it seems a little arbitrary to me to define traits as a higher level construct which can only apply to these scoped style selectors. If one wants to come up with a syntax that could possibly be included in CSS, it would be nice to ensure it generalises as fully as possible. Locally scoping CSS seems like a completely different specification and concept, but which is also incredibly important.

For example, one could define traits to apply to arbitrary selectors:

.some selector {
  /* # as example, come up with some property syntax here for the trait */
  #traitName: traitValue;
  color: red;
}

Then the specificity preference can be defined - like having inline styles of the same specificity taking preference over the trait style.

The output of the above would then become:

.some selector {
  /* trait expansion */
  background-color: green;
  color: blue;
  /* inline style */
  color: red;
}

The above can then be compacted into (so that the previous output is never seen):

.some selector, .other selector sharing trait {
  background-color: green;
  color: blue;
}
.some selector {
  color: red;
}

I know it removes the simplicity of having the trait entirely represented by a class, but then traits can be used on arbitrary elements and one could imagine them a core part of CSS some day.

Do tell me if I'm making no sense though.

In terms of exploring the scoped CSS ideas, loving where this syntax is going, again would be good to consider from a spec-viability perspective as much as possible, not that it is a must, but that it should at least be a possibility and not an impossibility if we miss something obvious that would be a blocker someday.

@markdalgleish
Copy link

@sokra I love this idea from your post:

@import { flex } from "./definitions.css"

I was wondering how we could nest CSS exports!

@necolas
Copy link

necolas commented May 21, 2015

Well, I suggested :local(className) would be preferable to the current syntax. But I'd prefer a solution where authoring is in JS, like this – https://gist.github.com/necolas/54b3baebd0e772e39f0e – with CSS being part of the compiled output. Authoring styles in CSS is a vestige / habit, rather than something that's necessary to achieve the intended outcome.

@geelen
Copy link
Author

geelen commented May 21, 2015

Well, I think I'm working backwards from the one actually new idea here — exporting tokens from CSS to JS. I love the approach already in @sokra and @markdalgleish's work, but exporting only a single class from CSS to JS seems like a waste. I really like the idea of exporting atomic classes — if you treat classnames on elements as the compilation target of whatever fancy shit you're trying to do, you get decent semantics (the class names dont need to be obfuscated to begin with) and absolutely the best-possible performance. @necolas just sketched out a similar idea, going from JS to CSS instead.

So, the trait thing is orthogonal to the exporting thing, but in my head they go nicely together. So how about we come up with a standard for the exporting first, that both webpack and JSPM can build upon, and could be one day turned into a CSS spec proposal:

@export [LOCAL TOKEN] [EXPORTED TOKEN];

So, current css-loader syntax is simple:

/* input */
.local[className] {
  foo: bar;
  baz: 2rem;
}

/* output */
@export className .d4e5f6;
.d4e5f6 {
  foo: bar;
  baz: 2rem;
}

Same thing for postcss-local-scope:

/* input */
.className {
  foo: bar;
  baz: 2rem;
}

/* output */
@export className .d4e5f6;
.d4e5f6 {
  foo: bar;
  baz: 2rem;
}

My trait idea here ends up generating:

.t-flex {
  display: flex;
}
.t-flex--inline {
    display: inline-flex;
  }
.t-flex--vertical {
    flex-direction: column;
  }
/* etc... */

@export outer .t-flex.t-flex--vertical.t-flex--align-stretch;
@export header .t-colors.t-colors--inverted.t-flex-child--no-shrink.my_component_18ab4f_head;

.my_component_18ab4f_head {
  height: 10vh;
  min-height: 100px;
  /* more one-off styles */
}

A couple of my thoughts on the exact syntax:

  • I think it's better to export multiple classnames using native CSS syntax instead of introducing something new. So, exporting a single class should be .className not "className", and multiple classes should be .classOne.classTwo not "classOne classTwo". This means an additional transformation is required to consume this in React, but it's dead-simple.
  • @export rules should be able to anywhere at the top-level in the file (i.e. not nested, but not required to be the first statement in the file).
  • It's not important to try to replicate JS export syntax. This isn't JS.

If we agreed on a standard, it would make interop between Webpack, JSPM & Browserify easier, and let me do my Trait idea and let other people do their own multi-export ideas as PostCSS transforms.

Thoughts?

@geelen
Copy link
Author

geelen commented May 21, 2015

Oh, and on clashing. Only :root, :not and :host can appear at the beginning of a CSS selector. :not is the odd one out, since it takes an argument, but the semantics of :root and :host are very similar to an exported token — what they match is dependent on context.

Maybe :: would be better, since they're pseudo-elements not pseudo-_selectors, though you'd then have to avoid ::selection and potentially ::backdrop

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