Demo project: Electro Sleuth application (newsletter signup) — Next.js + React
Table of Contents
- Course Overview
- Making Styling Decisions
- Comparing React Styling Techniques
- Coding with CSS-in-JS (styled-jsx)
- Coding with CSS Modules
- Recap and Best Practices
1. Course Overview
This course covers different styling approaches in React 18:
- Decide how to configure your styling system
- Compare different styling techniques
- Use these techniques in a real React project
2. Making Styling Decisions
Why Style an Application
| Goal | Description |
|---|---|
| Professionalism | Builds user trust |
| Usability | Facilitates navigation and understanding |
| Attractiveness | Creates a good impression and retains users |
Decision Criteria
| Criteria | Questions to Ask |
|---|---|
| Team | Experience level, preferences, team size |
| Product | Required features, UI complexity |
| Browsers | Compatibility constraints |
| Tools | Build ecosystem, bundler, framework |
| Delivery | SPA, SSR, islands of interactivity |
| Lifecycle | Long-term, maintainability, evolution |
Key advice: Don’t fall into analysis paralysis. All options are valid. Choosing and moving forward is better than seeking perfection.
3. Comparing React Styling Techniques
Fundamental CSS Challenges
In 2014, Christopher Chedeau (Facebook) identified structural CSS challenges:
| # | Challenge | Description |
|---|---|---|
| 1 | Global namespace | All selectors are global by default |
| 2 | Dependencies | Difficult to trace which styles apply where |
| 3 | Dead code elimination | Can’t know if a selector is still used |
| 4 | Minification | Long class names can’t be automatically shortened |
| 5 | Sharing constants | Hard to share values between CSS and JS |
| 6 | Non-deterministic resolution | Style order can vary by bundler |
| 7 | Isolation | One component can affect another’s styles |
Inline Styles
function MyComponent() {
const style = {
color: 'crimson',
fontSize: '16px', // camelCase, not font-size
backgroundColor: '#fff',
};
return <div style={style}>Hello</div>;
}
// Inline direct (double braces: one for JSX, one for object)
function MyComponent() {
return <div style={{ color: 'crimson', fontSize: 16 }}>Hello</div>;
}
| Advantage | Disadvantage |
|---|---|
| No library required | No CSS pseudo-classes |
| Total co-location | No media queries |
| Full isolation | No @keyframes animations |
| Easy dynamic styles | Maximum specificity |
CSS-in-JS
Popular libraries: styled-components, styled-jsx (Next.js), emotion, Linaria, vanilla-extract
Example with styled-jsx (Next.js):
function Container({ children }) {
return (
<section>
{children}
<style jsx>{`
section {
background: #2b283d;
padding: 1em;
max-width: 100%;
}
@media (min-width: 800px) {
section {
font-size: 2.25em;
max-width: 700px;
}
}
`}</style>
</section>
);
}
| Advantage | Disadvantage |
|---|---|
| Full CSS (pseudo-classes, media queries, animations) | Requires third-party library |
| Automatic isolation | Runtime overhead (for some libraries) |
| Dynamic styles via JS props | Learning curve |
| Total co-location | More CSS in JS bundle |
CSS Stylesheets
/* MyComponent.css */
.container { background: #2b283d; padding: 1em; }
.button { background: white; color: #070222; font-weight: bold; }
import './MyComponent.css';
function MyComponent() {
return (
<div className="container">
<button className="button">Click me</button>
</div>
);
}
| Advantage | Disadvantage |
|---|---|
| Familiar, native CSS syntax | Global namespace (naming collisions) |
| No special build step | Hard to isolate styles by component |
| Optimal browser caching | Dead code hard to detect |
CSS Modules
Author file: .button { ... }
After compile: .button__abc123 { ... }
/* Newsletter.module.css */
.container { background: #2b283d; padding: 1em; }
.button { background: white; color: #070222; }
.buttonActive {
composes: button;
border-bottom: 3px solid var(--accent);
}
import css from './Newsletter.module.css';
function Newsletter() {
const [active, setActive] = React.useState(false);
return (
<section className={css.container}>
<button className={active ? css.buttonActive : css.button}>
Sign up
</button>
</section>
);
}
| Advantage | Disadvantage |
|---|---|
| Automatic isolation via hashing | Requires module bundler |
| Native CSS syntax | Sharing between CSS files still difficult |
| Dead code elimination possible | No native JS dynamic styles |
composes for reuse | Generated class names in DOM |
Comparison Table
| Criteria | Inline Styles | CSS-in-JS | CSS Stylesheets | CSS Modules |
|---|---|---|---|---|
Pseudo-classes (:hover) | No | Yes | Yes | Yes |
| Media queries | No | Yes | Yes | Yes |
@keyframes animations | No | Yes | Yes | Yes |
| Global isolation | Yes | Yes | No | Yes |
| Co-location | Yes | Yes | Partial | Partial |
| JS dynamic styles | Yes | Yes | No | Limited |
| JS/CSS constant sharing | Yes | Yes | No | No |
| Build step required | No | No | No | Yes |
| Third-party library | No | Yes | No | No |
| Performance (cache) | No | Varies | Yes | Yes |
4. Coding with CSS-in-JS (styled-jsx)
Project Structure
src/
├── pages/
│ └── index.js
└── components/
├── index.css ← global styles (reset, body)
└── Newsletter.js ← main component with styled-jsx
Global styles (index.css):
* { box-sizing: border-box; }
body {
margin: 0;
background: #070222;
color: #fff;
font-size: 18px;
}
Container Component with Media Query
function Container(props) {
return (
<section>
{props.children}
<style jsx>{`
section {
position: relative;
max-width: 100%;
font-size: 1.25em;
padding: 1em 1em 2em 1em;
background: #2b283d;
}
@media (min-width: 800px) {
section {
font-size: 2.25em;
max-width: 700px;
}
}
`}</style>
</section>
);
}
Email Component with Focus State
function EmailInput(props) {
return (
<>
<input {...props} />
<style jsx>{`
input {
height: 2em;
font-size: 0.85em;
padding: 0 0.5em;
width: 100%;
border: 1px solid black;
}
input:focus {
outline: 2px solid #fff;
outline-offset: 0.15em;
}
`}</style>
</>
);
}
Submit Component with Dynamic Styles
const color = {
spectrum1: '#ff598a',
spectrum2: '#de56e8',
spectrum3: '#b36bff',
spectrum4: '#5b56e8',
spectrum5: '#5e9fff',
};
function Submit(props) {
return (
<button>
{props.children}
<style jsx>{`
button {
background: #fff;
color: #070222;
font-weight: bold;
cursor: pointer;
transition: all 300ms;
/* Conditional styles based on props.active */
height: ${props.active ? 'auto' : '0'};
width: ${props.active ? 'auto' : '0'};
font-size: ${props.active ? '1em' : '0'};
border-bottom: ${props.active
? `3px solid ${color.spectrum5}`
: '0'};
}
${props.active ? `
button:hover {
border-bottom-color: ${color.spectrum1};
scale: 1.2;
}
` : ''}
`}</style>
</button>
);
}
Key point: JavaScript template literals (
${expression}) allow injecting prop values directly into CSS — something impossible with traditional stylesheets.
Bar Component with Keyframes Animation
function Bar(props) {
return (
<div>
<style jsx>{`
div {
height: ${props.active ? '100%' : '0.5em'};
animation: jitter 350ms ease-out infinite alternate;
width: 20%;
transform-origin: bottom;
transition: all 1s;
}
div:nth-child(1n) { background: ${color.spectrum1}; animation-delay: 0; }
div:nth-child(2n) { background: ${color.spectrum2}; animation-delay: 50ms; }
div:nth-child(3n) { background: ${color.spectrum3}; animation-delay: 100ms; }
@keyframes jitter {
0% { transform: scaleY(1); }
100% { transform: scaleY(0.9); }
}
`}</style>
</div>
);
}
Header with :global Selector
function Header(props) {
return (
<header>
{props.children}
<style jsx>{`
header {
text-transform: uppercase;
font-size: 0.85em;
}
/* :global() targets child elements rendered by React */
header :global(h2) {
margin: 0 0 0.5em 0;
}
`}</style>
</header>
);
}
5. Coding with CSS Modules
Import and Usage
import css from './Newsletter.module.css';
export default function Newsletter() {
const [email, setEmail] = React.useState('');
const isActive = email.includes('@') && email.includes('.');
return (
<section className={css.container}>
<input
type="email"
className={css.email}
value={email}
onChange={evt => setEmail(evt.target.value)}
/>
<button className={isActive ? css.submitActive : css.submit}>
Sign up
</button>
</section>
);
}
Newsletter.module.css
.container {
--accent: #5e9fff;
--highlight: #ff598a;
position: relative;
max-width: 100%;
font-size: 1.25em;
padding: 1em 1em 2em 1em;
background: #2b283d;
}
@media (min-width: 800px) {
.container { font-size: 2.25em; max-width: 700px; }
}
.email {
height: 2em;
font-size: 0.85em;
padding: 0 0.5em;
width: 100%;
border: 1px solid black;
}
.email:focus {
outline: 2px solid #fff;
outline-offset: 0.15em;
}
.submit {
position: absolute;
bottom: 0;
background: #fff;
color: #070222;
font-weight: bold;
cursor: pointer;
transition: all 300ms;
height: 0;
width: 0;
font-size: 0;
}
/* Active state — composition from .submit */
.submitActive {
composes: submit;
height: auto;
width: auto;
font-size: 1em;
padding: 0.25em 1em;
border-bottom: 3px solid var(--accent);
}
.submitActive:hover {
border-bottom-color: var(--highlight);
scale: 1.2;
}
Class Composition with composes
.button {
padding: 0.5em 1em;
border: none;
cursor: pointer;
}
/* Variant — inherits from .button and adds/overrides rules */
.buttonPrimary {
composes: button;
background: #ff598a;
color: white;
font-weight: bold;
}
/* Composition from another file */
.buttonDanger {
composes: button from './shared.module.css';
background: #c1121f;
}
CSS Custom Properties in Modules
/* Design tokens at root component level */
.container {
--color-primary: #ff598a;
--color-secondary: #5e9fff;
--spacing-sm: 0.5em;
--spacing-md: 1em;
}
/* Consumption in child classes */
.title {
color: var(--color-primary);
margin-bottom: var(--spacing-sm);
}
6. Recap and Best Practices
Decision Flowchart
flowchart TD
Start([Choose a CSS approach]) --> Q1{Need dynamic styles\nbased on JS state?}
Q1 -- Yes --> Q2{Prefer staying\nin JS?}
Q1 -- No --> Q3{Do you have\na bundler configured?}
Q2 -- Yes --> B[CSS-in-JS\nstyled-components / emotion / styled-jsx]
Q2 -- No --> A[Inline Styles\nfor simple cases]
Q3 -- Yes --> D[CSS Modules\nBest balance CSS + isolation]
Q3 -- No --> C[Traditional CSS Stylesheets]
Sweet Spots Table
| Approach | Ideal Use Case |
|---|---|
| Inline Styles | Quick prototypes, fully dynamic styles, completely isolated components |
| CSS-in-JS | Apps with complex theming, design systems, highly conditional styles |
| CSS Stylesheets | Simple apps, CSS-familiar teams, critical performance |
| CSS Modules | Most React apps — best compromise between isolation and CSS familiarity |
General Best Practices
- Co-locate styles with their components
- Use CSS Custom Properties for design tokens (colors, spacing, sizes)
- Avoid global namespace — prefer CSS Modules or CSS-in-JS for component styles
- Think in components — each component manages its own styles
- Use
composesinstead of copy-pasting in CSS Modules - Dynamic styles are best handled in CSS-in-JS or via class toggling in CSS Modules
- Don’t over-engineer — a simple imported
.csscan suffice for many cases
“In the end, all you need is a class name on an element with selectors to match. How you get there is much less important.”
Search Terms
styling · apps · react · typescript · frontend · development · css · component · coding · css-in-js · decision · styles