ThemeMiner is the missing piece for styled-components.
It makes it very easy and readable, to create complex design systems in React
with styled-components
.
ThemeMiner will consume the theme
provided with <ThemeProvider />
.
yarn add theme-miner
or npm install theme-miner --save
To manage your design system and all the other variables first we create a theme file.
theme.js
import { ThemeMiner } from "theme-miner";
import { depth, colors, box } from "./constants";
export const theme = {
button: {
depth: depth, // we could still overwrite default options for this scope only.
variant: colors, // we could still overwrite default options for this scope only.
size: {
small: {
...box.size.small,
fontSize: 8
},
normal: {
...box.size.normal,
fontSize: 14
},
large: {
...box.size.medium
fontSize: 20
}
}
}
};
// these are interactives, we will use it as variables
const interactives = {
depth: { // or "myDepths"
options: ["base", "flat", "raised", "overlay", "superior", "declaration"],
default: "flat"
},
size: { // or "s"
options: ["small", "normal", "large"],
default: "normal"
},
variant: { // or "color"
options: ["black", "white", "primary", "destructive", "positive"],
default: "primary",
variants: {
key: "tone", // or "shade"
options: ["lighter", "light", "main", "dark", "darker"],
default: "main"
}
}
};
const options = {
useTransient: true, // adds $ to interactives e.g. variant becames $variant
useOptions: true, // allow us to use $black addition to $variant="black"
useVariants: true, // allow us to use $light addition to $tone="light"
// PS: don't use same identifiers both inside options and variants
properties: [
// always will be included in uiProps and passed to styled components
"$color",
"$passive",
"$gutter",
"$padding",
"$radius",
"$v",
"$h"
]
};
const UI = new ThemeMiner({
theme,
interactives,
options,
mixins: {
multiply: ui => {
return (value, count) => {
return `${value * count}`;
};
}
}
});
export default UI;
Now we can pass our theme file to the provider.
App.js
import { ThemeProvider } from "styled-components";
import { theme } from "./theme";
const App = () => {
return (
<ThemeProvider theme={theme}>
<MyApp {...this.props} />
</ThemeProvider>
);
};
export default App;
This step is very recommended but not mandatory. A base component will expose us some api e.g. this.ui
or this.closest
BaseComponent.js
import UI from "../theme";
const { properties, active, closest } = UI;
class BaseComponent extends React.Component {
// clean props for to use in a styled component
get ui() {
const uiProps = properties(this.props, this.uiProps || []);
return { ...uiProps, __active: this.active };
}
// helps find lower or higher option in a specific interactive
closest(key, value, checkVariant) {
return closest(
key,
checkVariant ? this.active[key].variant : this.active[key].key,
value,
checkVariant
);
}
// active interactives for this instance
get active() {
return active(this.props);
}
}
export default BaseComponent;
StyledButton.js
import styled from "styled-components";
import UI from "../theme";
const { cond, is, scoped } = UI;
const { _, mixin } = scoped("button"); // _, mixin, calc could be scoped
export const StyledButton = styled.div`
font-size: ${_`size.fontSize``px`};
border-radius: ${_("size.radius")("px") /* we can use without template literals */};
height: ${_`size.box.height`(height=> `${100/height}px`) /* or customize the output */};
width: ${_`size.box.width``%`};
background ${_`variant.active`};
color ${_`variant.contrast`};
padding: ${mixin("multiply", 2)`size.padding``px`}
text-decoration: ${cond({
if: is.eq("props.active",true) // or "props.active" or _`props.active` as a string,
// we could do this too; if: or(is("props.$variant","positive"),"props.active")
then: "underline",
else: null // optional
})};
`;
// PS: we can also consume theme without scoped like UI._`button.size.fontSize``px`
Button.js
import BaseComponent from "../BaseComponent";
import StyledButton from "./StyledButton";
class Button extends BaseComponent {
constructor(props) {
super(props);
this.uiProps = ["active"];
}
render() {
// this.ui allow us to pass only props necessary for styled component
return <StyledButton {...this.ui}>{this.props.title}</StyledButton>;
}
}
Home.js
import Button from "./Button";
export const Home = () => {
return (
<div>
<Button $positive $large $light>
Hello World
</Button>
or
<Button $variant="positive" $size="large" $tone="light">
Hello World
</Button>
</div>
);
};
creates scoped helpers from theme.scope_name
scoped("scope_name")
returns two helper
{ _, mixin }
returns resolved UI props and theme variables for styling
style(`buttons.size.padding`, ({ button, theme, active, props }) => {})
same as style
_`button.size.padding`
calculates data in template literals, it's handy to use in conditions and unitless operations
calc(`${_`size.padding`} * 2`)
use global mixins defined in theme file.
mixin("some_helper",1,2,3)("scope_name")(x=>{ // do something})
helps define conditions in styled-components
cond({if: "props.active", then: "10px", else: "8px"})
cond({if: "props.active == true", then: "10px", else: "8px"})
cond({if: "!props.active", then: "10px", else: "8px"})
cond({if: "props.height > 10", then: "10px", else: "8px"})
cond({if: _`props.height`, then: (p)=> p.height , else: "0"})
cond({if: is.ne("props.status","error"), then: "blue" , else: "red"})
cond({if: (p)=> p.height/2 == 10, then: "foo" , else: "bar"})
cond({if: and("props.active",is.eq("status","error")), then: "foo" , else: "bar"})
return all active interactive keys and their variants if exists.
active(this.props)
collect all interactive variables and specified props.
properties(this.props, ["align","active","etc"])
collect all interactive variables and specified props.
closest("size", active(this.props).size.key, -1)
closest("color", active(this.props).color, 1, true)
closest("color", active(this.props).color.variant, -1, true)
same as global style or _ but it is scoped, so there is no need for scope name in dot notation.
_`size.padding`
same as global mixins but scoped.
mixin`variant.active`(color=> transparentize(0.9,color))
or(a,b)
or helper for conditions
and(a,b)
and helper for conditions
is.eq(key,value)
checks if it is equal
is.ne(key,value)
checks if it is not equal
is.nset(key)
checks if it is not exists
is.set(key)
checks if it is exists
is.in(key,value)
checks if it is included
is.nin(key,value)
checks if it is not included
is.lt(key,value)
checks if it is little than
is.lte(key,value)
checks if it is equal or little than
is.gt(key,value)
checks if it is greater than
is.gte(key,value)
checks if it is equal or greater than
is.color(key)
checks if it is color
Pull requests are welcome and please submit bugs 🐛.
- Follow @doubco on Twitter
- Follow @doubco on Facebook
- Follow @doubco on Instagram
- Email mailto:[email protected]