NSCSS - CSS for Components
Styling library for composable components.
How it works?
Static styles by source order, compose unique selectors for each scope that override the scoped subtree
Stylesheet has namespace and defines semantic scope parts:
Single declaration:
- class - basic definition to build selectors from
- typed class - root, scoped & component
- selector - composed from defined parts
Features
- Scoped Stylesheet
- Root definition
- Typed selectors
- Custom states
- Complex selectors
- Theming
- Framework Agnostic
- Extract plain CSS
- Easy debugging - readable CSS Class Names, fail-fast at evaluation time
- No Runtime - No Runtime Errors 😜
Native css support
- @media query
- @font-face
- Animations
- Pseudo Elements / Pseudo Classes / Native States
- Child selectors
- Attribute selectors
Plugins & 3rd party integration
- Hooks to allow style transformations (vendor prefixing, etc.)
- React integration
- IDE Plugins
Syntax
- TypeScript
- Styles As Object Literals
The whole picture
This examples based on React and uses the nscss react components.
Install:
npm install nscss --save
import * as React from "react";
import { render } from "react-dom";
import { NSComponent, NSStateless, Stylesheet } from "nscss/react";
const HelloWorld = NSStateless(()=>{
return <div className="Hello">Hello World</div>
}, {
".Hello": {
color: "green"
}
});
const App = NSComponent(class extends React.Component<any, any>{
render(){
return <div className="App">
// classNames and styles are automatically applied on the root element of components
<HelloWorld className="MyHello"/>
</div>
}
}, {
".App": {
backgroundColor: "blue"
},
".MyHello": {
color: "{{myColor}}"
}
});
Stylesheet.context.attach({
myColor: "red"
});
render(<App/>, document.querySelector("#app"));
Basic Concepts
Class definition
All Primitive part definitions are class definitions that are used to makeup the selectors.
A class may implement another sheet interface and define states.
Scoping
Namespace is prefixed to each class name in the stylesheet
Code:
new Stylesheet({
"@namespace": "App",
".redButton": { color: "red" },
".blueButton": { color: "blue" }
});
CSS output:
.App◼redButton { color: red; }
.App◼blueButton { color: blue; }
Root definition
The main class for the component root node.
- Used as namespace when namespace is not provided
- Used as default cascade parent for component and scoped classes
Stylesheet with root:
new Stylesheet({
"@namespace": "Button",
"@define": {
"root": Stylesheet.root()
},
".root": { color: "red" }
});
CSS output:
.Button◼root { color: red; }
Typed selectors
Special class definitions that allows you to define your stylesheet structure in order it to be used by other stylesheets.
Component classes
Component class is an alias to a css class name, when used in a selector it will be replaced with the class name of the extended component. it's behavior is like css element (tag) selector it affects all instances of the component
const Label = new Stylesheet({
"@define": {
"Label": Stylesheet.root()
}
});
const Button = new Stylesheet({
"@define": {
"Button": Stylesheet.root(),
"MyLabel": Stylesheet.component(Label)
},
".MyLabel": { color: "red" },
".MyLabel:hover": { color: "green" },
".Button:hover .MyLabel": { color: "blue" }
});
Possible markup (JSX)
<button className="Button">
<Label/>
<button>
CSS output:
.Button◼Button .Label◼Label { color: red; }
.Button◼Button .Label◼Label:hover { color: green; }
.Button◼Button:hover .Label◼Label { color: blue; }
Scope classes
Intended to be passed to a component of the scoped type. when used in a selector it's output will have stronger specificity then a component class.
const Label = new Stylesheet(...);
new Stylesheet({
"@define": {
"Button": Stylesheet.root(),
"Label": Stylesheet.scoped(Label)
},
".Label": { color: "red" },
".Label:hover": { color: "green" },
".Button:hover > .Label": { color: "blue" }
});
Possible markup (JSX)
<button className="Button">
<Label/>
<Label className="Label"/>
<button>
CSS output:
.Button◼Button .Button◼Label { color: red; }
.Button◼Button .Button◼Label:hover { color: green; }
.Button◼Button:hover > .Button◼Label { color: blue; }
Customization of inner parts
When using typed classes, it is possible to style the inner parts with the ::
operator.
This ability can be chained for as many level as needed.
const Label = new Stylesheet({
"@define": {
"Label": Stylesheet.root()
},
".Text": { color: "red" }
});
const Button = new Stylesheet({
"@define": {
"Button": Stylesheet.root(),
"Label": Stylesheet.scoped(Label),
}
});
new Stylesheet({
"@define": {
"MyForm": Stylesheet.root(),
"Button": Stylesheet.component(Button),
"SubmitButton": Stylesheet.scoped(Button)
},
".Button::Label::Text": { color: "green" },
".SubmitButton::Label::Text": { color: "blue" }
});
CSS output:
.Label◼Text { color: red; }
.MyForm◼MyForm .Button◼Button .Button◼Label .Label◼Text { color: green; }
.MyForm◼MyForm .MyForm◼SubmitButton.Button◼Button .Button◼Label .Label◼Text { color: blue; }
Custom states
Custom states are mapping between an attribute selector to a name.
Any class definition can accept states that can be used to build selectors.
Root with custom states:
new Stylesheet({
"@define": {
"Button": Stylesheet.root('div', {
mouseHover: "[data-state-hover]",
disabled: "[data-state-disabled]"
})
},
".Button": { color: "red" },
".Button:mouseHover": { color: "green" }
});
CSS output:
.Button◼Button { color: red; }
.Button◼Button[data-state-hover] { color: "green" }
Custom states auto mapping
Requires integration with stylesheet instance to generate state attributes in the default format of data-${namespace}-${stateName}
.
Root with auto states:
const sheet = new Stylesheet({
"@define": {
"Button": Stylesheet.root('div', ["mouseHover", "disabled"])
},
".Button": { color: "red" },
".Button:mouseHover": { color: "green" }
});
function Button(){
return <button {...sheet.cssStates({mouseHover: true, disabled: false})}></button>
}
CSS output:
.Button◼Button { color: red; }
.Button◼Button[data-Button-mouseHover] { color: "green" }
Extend stylesheet
When a stylesheet definition root extends another stylesheet*, it automatically extends states.
- view should have a root component matching the extended sheet.
const Button = new Stylesheet({
"@define": {
"Button": Stylesheet.root('button', ["hover", "focus"])
}
});
new Stylesheet({
"@define": {
"ToggleButton": Stylesheet.root(Button, ["toggled", "focus"])
},
".ToggleButton:active": { color: "black" },
".ToggleButton:hover": { color: "red" },
".ToggleButton:toggled": { color: "green" },
".ToggleButton:focus": { color: "blue" }
});
CSS output:
.ToggleButton◼ToggleButton:active { color: black; }
.ToggleButton◼ToggleButton[data-Button-hover] { color: red; }
.ToggleButton◼ToggleButton[data-ToggleButton-toggled] { color: green; }
.ToggleButton◼ToggleButton[data-ToggleButton-focus] { color: blue; }
Complex Selectors
Selector definition must start with a defined part and then it can target internal parts, states or other defined parts.
new Stylesheet({
"@define": {
"App": Stylesheet.root()
},
".Button": {
color: "red"
},
".Container .Button": {
color: "blue"
},
".Container[data-active] > .Button:hover": {
color: "green"
}
});
Possible markup (JSX)
<div className="App">
<div data-active className="Container">
<button className="Button"></button>
</div>
<div className="Container">
<button className="Button"></button>
</div>
<button className="Button"></button>
<div>
CSS output:
...
.App◼Button { color: red; }
.App◼Container .App◼Button { color: blue; }
.App◼Container[data-active] > .App◼Button:hover { color: green; }
Global Theme
All css values in nscss are actually part of a template engine
the data for the variables comes when you attach the stylesheet to the dom
new Stylesheet({
"@define": {
"Button": Stylesheet.root()
},
".Button": {
color: "{{primaryColor}}"
}
});
Stylesheet.attach({
primaryColor: "red"
});
CSS output:
.Button◼Button { color: red; }
Mixins
Mixins are recipes to apply complex rule sets and selectors with simplified interface.
using mixins is very straight forward:
new Stylesheet({
"@define": {
"Container": Stylesheet.root()
},
".Container": {
...GridMixin({ itemsPerRow: 3, gutter: '50px' })
}
})
CSS output:
.Container◼Container { }
.Container◼Container > * { }
How to write a mixin
This is the definition of GridMixin it shows all the available apis
interface Options {
itemsPerRow: number;
gutterX: string;
gutterY: string;
}
function mixinGridCell(options: Options){...}
const GridMixin = defineMixin('GridMixin', (ctx, options: Options) => {
ctx.insertRules({
'display': 'flex',
'flexWrap': 'wrap',
'justifyContent': 'space-around',
'alignContent': 'space-around',
});
ctx.insertSelector(' > *', mixinGridCell(options));
});
React integration
TBD
- target stylesheet selector part
- multiple different app context -write example
- Per Stylesheet Theme
- change
component
class to subtree
class? - change
scope
class to class
class? - auto internal part
::content
to reference root if not used - separate stylesheet schema/interface from selector definition for shared interface between sheets
- define stylesheet from css
- order of scoped override in css and dom for readability (.A◼A.B◼B vs .B◼B.A◼A)