Concerning CSS in JS
CSS in JS is concerning and this is a talk concerning CSS in JS
Kent C. Dodds
Utah
1 wife, 3 kids
PayPal, Inc.
@kentcdodds
Please Stand...
if you are able
What this talk is
- The "why" of CSS in JS
- Trade-offs
- Problems looking for solutions
What this talk is not
- An attempt to get you to use CSS in JS
- Badmouthing CSS or those who use/like it as it is.
Let's
Get
STARTED!
Our Component:
JavaScript + HTML = JSX
class Toggle extends Component {
state = { toggledOn: false };
handleToggleClick = () => {
this.setState(
({ toggledOn }) => ({ toggledOn: !toggledOn }),
(...args) => {
this.props.onToggle(this.state.toggledOn);
},
);
};
render() {
const { children } = this.props;
const { toggledOn } = this.state;
const active = toggledOn ? 'active' : '';
return (
<button
className={`btn btn-primary ${active}`}
onClick={this.handleToggleClick}
>
{children}
</button>
);
}
}
export default Toggle;
Components!
import { PrimaryButton } from './css-buttons';
class Toggle extends Component {
state = { toggledOn: false };
handleToggleClick = () => {
this.setState(
({ toggledOn }) => ({ toggledOn: !toggledOn }),
(...args) => {
this.props.onToggle(this.state.toggledOn);
},
);
};
render() {
const { children } = this.props;
const { toggledOn } = this.state;
const active = toggledOn ? 'active' : '';
return (
<PrimaryButton
active={active}
onClick={this.handleToggleClick}
>
{children}
</PrimaryButton>
);
}
}
export default Toggle;
Components!
function AppButton({ className = '', active, ...props }) {
return (
<button
className={`btn ${className} ${active ? 'active' : ''}`}
{...props}
/>
);
}
function PrimaryButton({ className = '', ...props }) {
return <AppButton className={`btn-primary ${className}`} {...props} />;
}
export default AppButton;
export { PrimaryButton };
CSS? Where are the styles?
CSS? 🤔
.btn {
display: inline-block;
font-weight: 400;
line-height: 1.25;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
padding: .5rem 1rem;
font-size: 1rem;
border-radius: .25rem;
transition: all .2s ease-in-out;
}
.btn.btn-primary {
color: #fff;
background-color: #0275d8;
border-color: #0275d8;
}
.btn-primary:hover, .btn-primary.active {
background-color: #025aa5;
border-color: #01549b;
}
- Conventions?
- Who's using these styles?
- Can I delete these styles?
- Can I change these styles?
CSS?
Pit of Success
a well-designed system makes it easy to do the right things and annoying (but not impossible) to do the wrong things.
Convention
Using conventions is just us compensating for not having a wide enough pit of success.
The P2P Funnel Page
Our CSS...
// import styles for funnel pages control, T2, T3, T4 and T5
.funnel_control,
.funnel_2A,
.funnel_2B,
.funnel_3,
.funnel_4,
.funnel_5_2dot5,
.funnel_5_xb,
.funnel_overseas {
@import "./funnel";
}
// import styles for funnel page T6 and variants
.funnel_6_2dot5,
.funnel_6_xb,
.funnel_7_xb,
.funnel_8_xb,
.funnel_9_xb,
.funnel_10_xb,
.funnel_11_xb,
.funnel_12_xb,
.funnel_7_gb {
@import "./funnelT6";
}
// import styles for ppme entry on funnel pages T5, T6, T7 and T8
.funnel_5_2dot5,
.funnel_5_xb,
.funnel_6_2dot5,
.funnel_6_xb,
.funnel_7_xb,
.funnel_8_xb,
.funnel_9_xb,
.funnel_10_xb,
.funnel_11_xb,
.funnel_12_xb,
.funnel_7_gb {
@import './block-group-entry';
@import './ppme-entry/ppme-entry';
@import './ppme-entry/ppme-entry-buttons';
@import './ppme-entry/ppme-entry-interaction';
}
// ...etc
What impact will my change have elsewhere?
Components
Default
Styled
Unstyled
Focus
Toggled
Components
What is a UI component made of?
HTML
CSS
JS
Component
Remember our CSS?
<style>
.btn {
display: inline-block;
font-weight: 400;
line-height: 1.25;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
padding: .5rem 1rem;
font-size: 1rem;
border-radius: .25rem;
transition: all .2s ease-in-out;
}
.btn.btn-primary {
color: #fff;
background-color: #0275d8;
border-color: #0275d8;
}
.btn-primary:hover, .btn-primary.active {
background-color: #025aa5;
border-color: #01549b;
}
</style>
<script>
function AppButton({ className = '', active, ...props }) {
return (
<button
className={`btn ${className} ${active ? 'active' : ''}`}
{...props}
/>
);
}
function PrimaryButton({ className = '', ...props }) {
return <AppButton className={`btn-primary ${className}`} {...props} />;
}
export default AppButton;
export { PrimaryButton };
</script>
import { css } from 'glamor';
const appButtonClassName = css({
display: 'inline-block',
fontWeight: '400',
lineHeight: '1.25',
textAlign: 'center',
whiteSpace: 'nowrap',
verticalAlign: 'middle',
userSelect: 'none',
border: '1px solid transparent',
padding: '.5rem 1rem',
fontSize: '1rem',
borderRadius: '.25rem',
transition: 'all .2s ease-in-out',
});
const highlightStyles = {
backgroundColor: '#025aa5',
borderColor: '#01549b',
};
const primaryButtonClassName = css({
color: '#fff',
backgroundColor: '#0275d8',
borderColor: '#0275d8',
':hover': highlightStyles,
});
const activeClassName = css(highlightStyles);
function AppButton({ className = '', active, ...props }) {
return (
<button
className={`${appButtonClassName} ${className} ${active ? activeClassName : ''}`}
{...props}
/>
);
}
function PrimaryButton({ className = '', ...props }) {
return (
<AppButton
className={`${primaryButtonClassName} ${className}`}
{...props}
/>
);
}
export default AppButton;
export { PrimaryButton };
Enter glamorous 💄
import glamorous from 'glamorous';
const AppButton = glamorous.button({
display: 'inline-block',
fontWeight: '400',
lineHeight: '1.25',
textAlign: 'center',
whiteSpace: 'nowrap',
verticalAlign: 'middle',
userSelect: 'none',
border: '1px solid transparent',
padding: '.5rem 1rem',
fontSize: '1rem',
borderRadius: '.25rem',
transition: 'all .2s ease-in-out',
});
const highlightStyles = {
backgroundColor: '#025aa5',
borderColor: '#01549b',
};
const PrimaryButton = glamorous(AppButton)(
{
color: '#fff',
backgroundColor: '#0275d8',
borderColor: '#0275d8',
':hover': highlightStyles,
},
({ active }) => (active ? highlightStyles : null),
);
export default AppButton;
export { PrimaryButton };
jest-glamor-react
📸
+ Our codebase now
function Consumer(props) {
return (
<FunnelPage>
<FunnelLinkGroup title={i18n('flexible.group.send')}>
<ConsumerFunnelLink {...funnelProps.buyLink(props)} />
<ConsumerFunnelLink {...funnelProps.sendLink(props)} />
<ConsumerFunnelLink {...funnelProps.xbLink(props)} />
<ConsumerFunnelLink {...funnelProps.giftLink(props)} />
<ConsumerFunnelLink {...funnelProps.massPaymentLink(props)} />
</FunnelLinkGroup>
<FunnelLinkGroup {...funnelProps.requestGroup(props)}>
<ConsumerFunnelLink {...funnelProps.requestLink(props)} />
<ConsumerFunnelLink {...funnelProps.invoiceLink(props)} />
<ConsumerFunnelLink {...funnelProps.poolsLink(props)} />
<ConsumerFunnelLink {...funnelProps.ppmeLink(props)} />
</FunnelLinkGroup>
</FunnelPage>
)
}
// glossing over some details...
function ConsumerFunnelLink(props) {
return (
<FunnelLink>
<Anchor verticalSpacing={14} {...anchorProps}>
<Icon icon={icon} svg={svg} />
<Title>{title}</Title>
<ConditionalDescription>
{description}
</ConditionalDescription>
</Anchor>
<DesktopBadge filler={!subtext}>{badge}</DesktopBadge>
<ConsumerSubtext>{subtext}</ConsumerSubtext>
</FunnelLink>
)
}
// and here's the Anchor component:
import glamorousLink from '../../component-factories/glamorous-link'
import { mediaQueries } from '../../../../styles'
import {
spaceChildrenVertically,
spaceChildrenHorizontally,
} from '../../css-utils'
const { phoneLandscapeMin: desktop, phoneLandscapeMax: mobile } = mediaQueries
const onAttention = '&:hover, &:focus, &:active'
const Anchor = glamorousLink(
{
display: 'flex',
'& > *:last-child': {
flex: '0 1 auto',
},
minHeight: 0, // firefox weirdness
[onAttention]: {
textDecoration: 'none',
'& .funnel-description': {
color: '#333333',
},
},
[mobile]: {
padding: '22px 12px 20px 12px',
flexDirection: 'row',
'& > *:last-child': {
flex: 1,
},
...spaceChildrenHorizontally(30),
},
[desktop]: {
marginTop: 12,
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
[onAttention]: {
'& .icon, & .icon-svg': {
color: '#ffffff',
backgroundColor: '#0070ba',
},
},
},
},
({ verticalSpacing = 10 }) => ({
[desktop]: {
...spaceChildrenVertically(verticalSpacing),
},
}),
)
export default Anchor
Now for the concerns...
Why is CSS in JS so concerning?
Familiarity
How would you solve this problem?
Workflow/IDE
- Create ESLint plugin for CSS in JS
- TypeScript/Flow support
How would you solve this problem?
Performance
Performance
How would you solve this problem?
Resources
- CSS in JS - Vjeux - "The original"
- glamor - Sunil Pai - css in your javascript
- glamorous - PayPal - React component Styling Solved 💄
- jest-glamor-react - Kent C. Dodds - Jest utilities for Glamor and React
- Code Sandbox - the examples from these slides in an interactive code editor in your browser.
- A Unified Styling Language - Mark Dalgleish - Why you should care about CSS in JS
Thank you!
Concerning CSS in JS
By Kent C. Dodds
Concerning CSS in JS
I no longer care about: specificity, CSS linters, CSS preprocessors, vendor prefixing, removing unused CSS, finding CSS dependencies and dependents. I now care more about: whether it’s fast enough, whether it’s small enough, whether it’s familiar enough. These are some of my trade-offs. Because I use CSS-in-JS. I’ve made trade-offs because I write HTML-in-JS. Despite these, I still do it, because the cost is minimal enough, and the benefit is great enough. Let’s tell stories, talk use-cases, explore trade-offs, and inspire more innovation to make the CSS-in-JS trade-offs less trade-offy.
- 5,642