Module 3 - Modern Component Architecture
Types of ways to write CSS
Vanilla CSS
-
Pros:
- No tooling, no runtime performance cost
- Modern CSS features like CSS Custom Properties make tooling features redundant
-
Cons:
- Global and unscoped
- Remember to add vendor prefixes for many CSS properties, checking for browser support
- Can't easily share data between CSS and JS
- CSS is built for documents, not apps
Sass / Less
-
A preprocessor that compiles to vanilla CSS. All valid CSS is valid Sass, and Sass compiles to CSS at build-time
-
Added features like nesting, variables and iteration
-
Less is similar to Sass, the main noticeable difference is syntax for variables
@var: 10
$var: 10
-
Pros:
- Includes powerful tools like for-loops, mixins and nesting
- High developer satisfaction compared to vanilla CSS
-
Cons:
- Requires a build step
- Global by nature because it compiles to CSS
- Can't react to real-time events because everything happens at build time
- Requires nativerr dependencies that become out of date
- Conflicting naming conventions with CSS
- Fading community, less support, less resources, more trouble
Aphrodite
-
Styles written in JS and used by React components
import { StyleSheet, css } from 'aphrodite'; function App() { return ( <div className={css(styles.wrapper)}> Hello World! </div> ); } const styles = StyleSheet.create({ wrapper: { backgroundColor: 'gray', minHeight: 200, '@media (min-width: 1025px)': { fontSize: '1.25rem', }, }, });
-
Pros:
- Scoping and specificity issues disappear
- Can use JS within CSS
- Encourages good habits
- Adds vendor prefixes automatically
- Similar API to React Native improves compatibility
-
Cons:
- Requires a build step
- Easy to forget css() function call
- Cost in terms of bundle size and runtime execution, though relatively small
- Styles must be writtein in JS, properties are camelCased and values must be quoted
- Weird to write global styles
- Not active anymore
CSS Modules
-
Tool that allows you to write vanilla CSS and import it into a JS file
-
Not a built-in feature
import styles from "./App.css";
-
Pros:
- Solves scoping and specificity
- Familiar with users, just writing CSS
- Offers a composes feature to extend existing CSS classes
-
Cons:
- Lack of modern convenience features like autoprefixing
- Hard to share data between CSS and JS
Single-file components
- Create scoped CSS in the same file as your component definition with Vue and Svelte
<template> <p>Hello World</p> </template> <style scoped> p { background-color: gray; min-height: 200px; } @media (min-width: 1025px) { p { font-size: 1.25rem; } } </style>
styled-components
-
A solution where every style is a React component, can write CSS and JS together
-
Pros:
- Solves scoping and specificity elegantly
- Familiarity writing CSS with some QoL features from Sass/Less
- Offers good solutions for animations and global styles
- High developer satisfaction
- Best-in-class performance
- Large community, lots of development and engagement, plenty of resources
-
Cons:
- Requires a build system
- Primarily a React tool
- Obfuscates the underlying markup tags
Emotion
-
Very similar API to styled-components
-
Pros:
- Can be used without React (with different syntax, though)
- Slightly smaller bundle size compared to styled-components
-
Cons:
- Not as popular as styled-components, smaller community
- emotion/styled takes a different approach towards dynamic props which is less flexible
Tailwind
-
A utility-first CSS framework
-
"Utilities" mean short CSS classes that map to specific styles
-
Pros:
- Solves scoping and specificity
- Encourages good habits for following a design system
- Includes built-in design tokens
- Faster to write over time
- Not specific towards React
- Growing popularity
-
Cons:
- Steep learning curve compared to other tools
- Not all CSS can be turned to utilities, will need another system for writing traditional CSS
- Lots of bulk added to markup, reducing readability
Introduction to styled-components
-
This library uses a JS feature called tagged template literals
- Here's a basic example, styled.button is a function passing in a template string as an argument
- The styled object has methods for every HTML tag typically used within the <body>
const Button = styled.button` font-size: 32px; `;
-
A Sass-like preprocessor called stylis is used to automatically add vendor prefixes for maximum browser compatibility
-
The & character can be used to reference a soon-to-be-created class
- Component names don't have to be globally unique, a unique hash is generated when a styled-component creates a class
- The & can be thought of as a placeholder, it will be replaced by a class name once generated
- Here's an example of the component and CSS produced
const Button = styled.button` display: flex; &:hover { color: red; } `;
.abc123 { /* Vendor prefixes for legacy browsers: */ display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } /* Plucks out the `hover` pseudo-class: */ .abc123:hover { color: red; }
-
CSS prop is an alternative writing method
const Title = ({ id, children }) => { return ( <h1 id={id} css={` font-size: 2rem; font-weight: bold; `} >{children}</h1> ) };
Installation and Setup
-
Installation of styled-components
npm install styled-components
-
Babel plugin
-
At runtime, the styled component you wrote will transform that component into a CSS class with a randomly generated name
-
A Babel plugin was released to solve this issue by picking semantically-meaningful class names in development
-
Class names are structured in the format: Filename_componentName-hash
- If the HTML is in <header class="ShoeIndex__Header-sc-123">, you can look for the const Header inside ShoeIndex.js
-
If you use Create React App, the build configuration is hidden unless you eject
-
However, it supports Babel macros so the next couple steps provide a solution
npm install --save-dev babel-plugin-styled-components
- Importing the macro gives the benefits of the Babel plugin without need to eject or mess with the build configuration
- In the React application, change imports to match the following:
// From this: import styled from 'styled-components'; // ...to this: import styled from 'styled-components/macro';
-
-
Editor integrations
- Tagged template literals are the core of styled-components
- But by default, the styles are treated like any other colourless string
- The vscode-styled-components plugin can add syntax highlighting and even proper auto-complete
-
Server-side Rendering
- styled-components run in the browser and has server-side rendering support, which means the initial HTML/CSS is generated beforehand
- Even if JS was disabled on the browser, most of the site could still work and all the styles would still appear
Global Styles
- styled-components has an API for creating global styles
- When GlobalStyles is rendered, all its CSS will inject into the <head> of the document
- Doesn't really matter where this component is rendered
import { createGlobalStyle } from 'styled-components'; const GlobalStyles = createGlobalStyle` *, *::before, *::after { box-sizing: border-box; } html { font-size: 1.125rem; } body { background-color: hsl(0deg 0% 95%); } `; export default GlobalStyles;
import GlobalStyles from '../GlobalStyles'; function App() { return ( <Wrapper> <Router> {/* An entire app here! */} </Router> <GlobalStyles /> </Wrapper> ) } export default App;
Dynamic Styles
-
Inline styles are quick and easy to add but have disadvantages
- CSS being split up between the component and the style attribute can cause confusion
- Not compatible with media queries, pseudo-classes and any CSS that's not a property/value
const Button = ({ color, onClick, children }) => { return ( <Wrapper onClick={onClick} style={{ color }}> {children} </Wrapper> ); } const Wrapper = styled.button` color: black; padding: 16px 24px; `;
- Setting properties in JavaScript requires a camelCase version of the property name
<input style={{ borderRadius: "8px", textDecoration: "none" }} />
-
Interpolation functions are the recommended way to manage dynamic styles in styled-components
- CSS values are passed as props
... <Wrapper onClick={onClick} color={color}> {children} </Wrapper> ... const Wrapper = styled.button` color: ${props => props.color}; padding: 16px 24px; `;
-
CSS variables allow the value of a property to be stored as a key in an object passed through the style attribute
- Less flexible, can only be used to provide property values, which is limiting with media queries
... <Wrapper onClick={onClick} style={{ '--color': color }}> {children} </Wrapper> ... const Wrapper = styled.button` color: var(--color); padding: 16px 24px; `;
Component Libraries
- A component library is a collection of generic, reusable components that can be used in multiple applications
- A design system is made up of a suite of vector graphics produced in tools like Adobe or Figma
- It may contain components like Button, Modal, etc.
- Combine design components into mockups and map them neatly to the component library
- Design tokens are values used to specify scales for sizes or colours
Breadcrumbs
-
Helpful navgiation links often found near the top of product listing pages
- Show hierarchy of content's structure
- Quickly navigate between parent or grandparent category page
-
Some notes on using styled-components to implement this:
- Can use negation logic to add styling to all elements that's not the first element
- Same concept can be applied to the last element with last-of-type
&:not(:first-of-type) { }
- Can use nesting in this example which essentially equates to &:not(:first-of-type)::before
&:not(:first-of-type) { &::before { content: '/'; opacity: 0.25; } }
- If there are two or more places which can scale dynamically with some property value, use a variable
- Now we only need to make an update to one variable if the spacing needs to change
--spacing: 12px; &:not(:first-of-type) { margin-left: var(--spacing); &::before { content: '/'; opacity: 0.25; margin-right: var(--spacing); } }
- Our navigation bar now has links separated by slashes with even spacing between the headers
Composition
-
If we need multiple variants of a component, we can create one component to serve as the base for another
const Base = styled.button` font-size: 21px; `; const PrimaryButton = styled(Base)` background: blue; color: white; `;
-
In the custom component, we can receive new props such as variant or size which determine what styled component we want to render
const Button = ({ variant, size, children }) => { const styles = SIZES[size]; let Component; if (variant === "fill") { Component = FillButton; } else if (variant === "outline") { Component = OutlineButton; } else if (variant === "ghost") { Component = GhostButton; } else { throw new Error(`Unrecognized Button variant: ${variant}`); } return ( <Component style={styles}> {children} </Component> ); };
Dynamic tags
- Use polymorphic as prop to dynamically change the tag that styled-components renders
- In the following example, if href is empty then it will render a button element, otherwise, an anchor element is rendered
- Can also render components as well, a common one to use is React Router's Link component
function Button({ href, children }) { return ( <Wrapper href={href} as={href ? 'a' : 'button'}> {children} </Wrapper> ); } const Wrapper = styled.button` /* Styles */ `; export const App = () => ( <Button href="/">Hello</Button> );
Escape Hatches
-
Sometimes, when using a component in our library it needs to be tweaked slightly to accommodate the situation
-
For example, an alert that has a button that needs to be red instead of some other default colour it's been set
-
Usually, there are colour variants to represent different statuses
- Blue: Information
- Green: Success
- Alert: Yellow
- Danger: Red
-
To implement this, the styled-component could take in a new prop that adjusts the colour of the element based off the property value
-
-
If there were events such as Christmas or Halloween and you wanted to add some new styling to match the theme for a day, we can develop one-off components
- We don't want to add more props like before because it adds significant complexity, it's not scalable and this will rarely get used
import Button from './Button'; const SpookyButton = styled(Button)` font-family: 'Spooky Halloween Font'; background-color: orange; color: black; `;
- Here, SpookyButton is based off the base Button component, but *Button isn't a styled-component
- Custom components can be composed by adding a className prop and passing it through to the rendered component
- Extending from the example in the composition section, className is an added prop passed to Button then passed through the rendered component
const Button = ({ variant, size, children, className }) => { const styles = SIZES[size]; let Component; if (variant === "fill") { Component = FillButton; } else if (variant === "outline") { Component = OutlineButton; } else if (variant === "ghost") { Component = GhostButton; } else { throw new Error(`Unrecognized Button variant: ${variant}`); } return ( <Component style={styles} className={className}> {children} </Component> ); };
- This works because the styled-component we created will have its declarations extracted into a class, then appended to the <head> and apply that class to the element
- Therefore, Button will receive the generated class name which is added to the rendered component
-
If you want to apply direct HTML, you can do so like here. The second option just uses a llower-friction API
<div dangerouslySetInnerHTML={{ __html: 'First · Second' }} />;
<div rawHtml="First · Second" />
Single Source of Styles
-
We want to allow components to have different styles depending on context without affecting the styling of other elements
-
Consider a element that's used in multiple contexts and wrapped by different parent elements, we might want to apply a different style in each scenario
- For the following example, we want the anchor tag to be black and underlined only within a Quote parent component
const TextLink = styled.a` ... /* Normal anchor styles */ ${QuoteContent} & { color: black; text-decoration: revert; } `;
- This is similar to the concept of inversion of control, where the parent element is nested rather than the child element being nested in the parent component
- The downside is it could bloat the bundles, since we have to import the parent component to reference the styles
Extra Mini Components
-
Custom progress bar
export default const ProgressBar = ({ value, size }) => { // Declare style object based off bar size return ( <Wrapper role="progressbar" aria-valuenow={value} aria-valuemin="0" aria-valuemax="100" style={{ '--padding': styles.padding + 'px', '--radius': styles.radius + 'px', }} > <VisuallyHidden>{value}%</VisuallyHidden> <BarWrapper> <Bar style={{ '--width': value + '%', '--height': styles.height + 'px', }} /> </BarWrapper> </Wrapper> ); }; const Wrapper = styled.div` background-color: ${COLORS.transparentGray15}; box-shadow: inset 0px 2px 4px ${COLORS.transparentGray35}; border-radius: var(--radius); padding: var(--padding); `; const BarWrapper = styled.div` border-radius: 4px; /* Trim off corners when progress bar is near-full. */ overflow: hidden; `; const Bar = styled.div` width: var(--width); height: var(--height); background-color: ${COLORS.primary}; border-radius: 4px 0 0 4px; `;
-
Custom select component
const Select = ({ id, value, onChange, children }) => { const displayedValue = getDisplayedValue(value, children); return ( <Wrapper> <NativeSelect id={id} value={value} onChange={onChange}> {children} </NativeSelect> <PresentationalBit> {displayedValue} <IconWrapper style={{ '--size': 24 + 'px' }}> <Icon id="chevron-down" strokeWidth={1} size={24} /> </IconWrapper> </PresentationalBit> </Wrapper> ); }; const Wrapper = styled.div` position: relative; width: max-content; `; const NativeSelect = styled.select` position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; /* Allow the select to span the full height in Safari */ -webkit-appearance: none; `; const PresentationalBit = styled.div` color: ${COLORS.gray700}; background-color: ${COLORS.transparentGray15}; font-size: ${16 / 16}rem; padding: 12px 16px; padding-right: 52px; border-radius: 8px; ${NativeSelect}:focus + & { outline: 1px dotted #212121; outline: 5px auto -webkit-focus-ring-color; } ${NativeSelect}:hover + & { color: ${COLORS.black}; } `; const IconWrapper = styled.div` position: absolute; top: 0; bottom: 0; right: 10px; margin: auto; width: var(--size); height: var(--size); pointer-events: none; `;