JavaScript and React course Β· Module 14: Advanced Styling
Main Project: Cosmic Dashboard with Different Styling Approaches
In this lesson5
In real projects you'll rarely find code styled with a single tool. The old layout was written in CSS Modules, someone added styled-components for dynamic cards, and a new team quickly assembles views in Tailwind. Being able to move between these worlds is what separates a junior from an engineer who can handle any ship in the fleet. Time for a summary! In this project, you'll build a Cosmic Mission Dashboard that combines different styling approaches. It's like building a complete mission control panel where each sector uses the best tool for its job.
Project Goal
Create a dashboard that presents information about space missions. The project should demonstrate the ability to use different styling methods in a single project. The key is a deliberate split of roles: each tool does what it's best at and doesn't get in the way of the others.
Requirements
1. Page Layout (CSS Modules)
The main dashboard layout - sidebar, header, main area - should be styled using CSS Modules. Remember that a file with the .module.css extension is plain CSS in which every class name gets a unique suffix at build time, so .container won't collide with any other class in the application. The grid has two columns (a 250px sidebar and the rest) and two rows (a 60px header and the content):
1/* Dashboard.module.css */
2.container {
3 display: grid;
4 grid-template-columns: 250px 1fr;
5 grid-template-rows: 60px 1fr;
6 min-height: 100vh;
7 background: #0a0a2e;
8}
9
10.sidebar {
11 grid-row: 1 / -1;
12 background: #0d1137;
13 padding: 20px;
14}
15
16.header {
17 background: #1a1a4e;
18 display: flex;
19 align-items: center;
20 padding: 0 24px;
21}
22
23.main {
24 padding: 24px;
25 overflow-y: auto;
26}The grid-row: 1 / -1 rule stretches the sidebar across all rows, from the first grid line to the last. In the component you'll use these classes by importing styles and writing className={styles.container}, just like in the CSS Modules lesson.
2. Interactive Components (Styled-components)
Buttons, status badges, cards with dynamic styles - this is where styled-components shines, because the style depends on props. Notice the dollar sign in $status. This is a so-called transient prop: styled-components version 6 no longer filters unknown props automatically and would pass a plain status through to the span element in the DOM (in development mode it then prints a warning). The $ prefix tells the library the prop is only for styling:
1const StatusBadge = styled.span`
2 padding: 4px 12px;
3 border-radius: 20px;
4 font-size: 12px;
5 font-weight: bold;
6 background: ${({ $status, theme }) => theme.statusColors[$status]};
7 color: ${({ $status }) => $status === 'active' ? '#0a0a2e' : 'white'};
8`;You'll use it as <StatusBadge $status="active">. The background color comes from the theme (theme.statusColors), so adding a new status only requires a new key in the theme, not a change to the component.
3. Cards and Lists (Tailwind CSS)
Quick presentational components with utility classes. Each Tailwind class does one thing: flex turns on flexbox, gap-3 sets the spacing, rounded-lg rounds the corners, and hover:bg-slate-700 changes the background on hover:
1function CrewMember({ name, role, avatar }) {
2 return (
3 <div className="flex items-center gap-3 p-3 bg-slate-800 rounded-lg
4 hover:bg-slate-700 transition-colors">
5 <img src={avatar} className="w-10 h-10 rounded-full" alt={name} />
6 <div>
7 <p className="text-white font-medium">{name}</p>
8 <p className="text-gray-400 text-sm">{role}</p>
9 </div>
10 </div>
11 );
12}There's no CSS file or styled-component here, the whole look lives in className. This is great for simple, static pieces of the interface and less suited to styles that depend heavily on state.
4. Theming (Styled-components ThemeProvider)
The entire dashboard should use a shared color theme. The object has the familiar colors and a new statusColors, which StatusBadge uses:
1const spaceTheme = {
2 colors: {
3 background: '#0a0a2e',
4 surface: '#1a1a4e',
5 primary: '#7c4dff',
6 text: '#e0e0e0',
7 success: '#00e676',
8 warning: '#ffa726',
9 danger: '#f44336'
10 },
11 statusColors: {
12 active: '#00e676',
13 pending: '#ffa726',
14 completed: '#64b5f6',
15 failed: '#f44336'
16 }
17};Only styled-components inside ThemeProvider can see the theme. CSS Modules and Tailwind classes don't know about it, so if you want a single source of colors for all three approaches, consider exposing the same values as CSS Variables.
5. Responsiveness
The dashboard should be responsive - on smaller screens the sidebar hides and the card grid switches to a single column. In the same CSS Modules file we add a media query:
1/* Dashboard.module.css */
2@media (max-width: 768px) {
3 .container {
4 grid-template-columns: 1fr;
5 }
6 .sidebar {
7 display: none;
8 }
9}Below 768px the grid has one column and the sidebar disappears. The remaining .container rules from the first block still apply, we only override what changes. In a real project, instead of hiding navigation completely, add a button that slides it out.
Component Structure
This is what the tree of the whole application looks like. Next to each component, the tool you style it with is given in parentheses:
1App
2 ThemeProvider (spaceTheme)
3 GlobalStyles
4 DashboardLayout (CSS Modules)
5 Sidebar (CSS Modules)
6 NavItem (CSS Modules)
7 Header (CSS Modules + Styled StatusBadge)
8 MainContent
9 MissionGrid (Tailwind grid)
10 MissionCard (Styled-components, dynamic props)
11 StatusBadge (Styled, color changes by status)
12 CrewList (Tailwind)
13 CrewMember (Tailwind)ThemeProvider and GlobalStyles sit at the very top, so the theme reaches every card and badge, no matter which section they end up in.
Tips
- Start with the layout (CSS Modules) - create a grid with sidebar, header, and main
- Add the ThemeProvider component and GlobalStyles
- Build interactive components (mission cards, status badges) with styled-components
- Use Tailwind classes for quick presentational elements (crew list, card grid)
- Remember color consistency - all colors should come from the theme
- Add at least one hover/focus effect and one animation
My advice: stick to one tool per component. Mixing Tailwind className values and styled-components on the same element quickly becomes unreadable.
Evaluation Criteria
- CSS Modules used for layout (grid, sidebar, header)
- Styled-components used for at least 3 components with dynamic props
- Tailwind CSS used for at least 2 presentational sections
- ThemeProvider with a defined color theme
- Responsiveness - the dashboard works on different screen sizes
Below you'll find an example of a finished dashboard that combines all three approaches. Analyze the code and get inspired to create your own version! You'll send the finished project to your mentor in the next task, and in the following locations the same principles of splitting roles will help you in every larger application.
Remember: a good mission dashboard isn't one tool for everything, but every sector equipped with what works best in it.
Code for this lesson: App.jsx
1import React, { useState } from 'react';
2import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
3import layoutStyles from './Dashboard.module.css';
4
5// === THEME (styled-components) ===
6const theme = {
7 colors: {
8 primary: '#7c4dff',
9 secondary: '#64b5f6',
10 bg: '#0a0a2e',
11 surface: '#1a1a4e',
12 text: '#e0e0e0',
13 muted: '#9e9e9e',
14 border: '#3949ab',
15 success: '#00e676',
16 warning: '#ffa726',
17 danger: '#f44336',
18 },
19};
20
21const GlobalStyles = createGlobalStyle`
22 * { margin: 0; padding: 0; box-sizing: border-box; }
23 body { font-family: 'Segoe UI', sans-serif; }
24`;
25
26// === STYLED-COMPONENTS (dynamic components) ===
27const StatusDot = styled.span`
28 display: inline-block;
29 width: 10px; height: 10px;
30 border-radius: 50%;
31 margin-right: 8px;
32 background: ${({ status, theme }) =>
33 status === 'active' ? theme.colors.success :
34 status === 'warning' ? theme.colors.warning :
35 theme.colors.danger};
36`;
37
38const StyledButton = styled.button`
39 padding: 8px 16px;
40 border: none;
41 border-radius: 6px;
42 cursor: pointer;
43 font-weight: bold;
44 font-size: 13px;
45 transition: all 0.2s;
46 background: ${({ variant, theme }) =>
47 variant === 'primary' ? theme.colors.primary :
48 variant === 'danger' ? theme.colors.danger : 'transparent'};
49 color: ${({ variant }) => variant === 'ghost' ? '#64b5f6' : 'white'};
50 border: ${({ variant }) => variant === 'ghost' ? '1px solid #3949ab' : 'none'};
51 &:hover { opacity: 0.85; }
52`;
53
54const ProgressBar = styled.div`
55 height: 6px;
56 border-radius: 3px;
57 background: #0d1137;
58 margin-top: 8px;
59 overflow: hidden;
60 &::after {
61 content: '';
62 display: block;
63 height: 100%;
64 width: ${({ value }) => value}%;
65 background: ${({ value, theme }) =>
66 value > 70 ? theme.colors.success :
67 value > 40 ? theme.colors.warning :
68 theme.colors.danger};
69 transition: width 0.5s ease;
70 }
71`;
72
73// === DATA ===
74const missions = [
75 { id: 1, name: 'Apollo X', target: 'Mars', status: 'active', progress: 72, crew: ['Nova', 'Rex', 'Luna'] },
76 { id: 2, name: 'Nebula Run', target: 'Orion', status: 'warning', progress: 45, crew: ['Astro', 'Vega'] },
77 { id: 3, name: 'Deep Void', target: 'Andromeda', status: 'critical', progress: 18, crew: ['Echo', 'Titan', 'Zara', 'Sol'] },
78];
79
80const navItems = ['Dashboard', 'Missions', 'Crew', 'Systems', 'Reports'];
81
82// === MAIN COMPONENT ===
83export default function App() {
84 const [activeNav, setActiveNav] = useState('Dashboard');
85 const [selectedMission, setSelectedMission] = useState(null);
86
87 return (
88 <ThemeProvider theme={theme}>
89 <GlobalStyles />
90 <div className={layoutStyles.container}>
91 {/* Sidebar (CSS Modules) */}
92 <aside className={layoutStyles.sidebar}>
93 <h2 className={layoutStyles.logo}>Mission Control</h2>
94 <nav>
95 {navItems.map(item => (
96 <div
97 key={item}
98 className={`${layoutStyles.navItem} ${activeNav === item ? layoutStyles.navActive : ''}`}
99 onClick={() => setActiveNav(item)}
100 >
101 {item}
102 </div>
103 ))}
104 </nav>
105 </aside>
106
107 {/* Header (CSS Modules) */}
108 <header className={layoutStyles.header}>
109 <h1 className={layoutStyles.headerTitle}>Cosmic Dashboard</h1>
110 <span className={layoutStyles.headerBadge}>3 active missions</span>
111 </header>
112
113 {/* Main content (mix) */}
114 <main className={layoutStyles.main}>
115 <div className={layoutStyles.grid}>
116 {missions.map(mission => (
117 <div
118 key={mission.id}
119 className={layoutStyles.card}
120 onClick={() => setSelectedMission(
121 selectedMission === mission.id ? null : mission.id
122 )}
123 >
124 <div className={layoutStyles.cardHeader}>
125 <h3 className={layoutStyles.cardTitle}>
126 <StatusDot status={mission.status} />
127 {mission.name}
128 </h3>
129 <span className={layoutStyles.cardTarget}>{mission.target}</span>
130 </div>
131 <p className={layoutStyles.cardMuted}>
132 Crew: {mission.crew.length} members
133 </p>
134 <ProgressBar value={mission.progress} />
135 <p className={layoutStyles.progressLabel}>{mission.progress}%</p>
136
137 {selectedMission === mission.id && (
138 <div className={layoutStyles.cardDetails}>
139 <p className={layoutStyles.detailTitle}>Crew members:</p>
140 {mission.crew.map(c => (
141 <span key={c} className={layoutStyles.crewBadge}>{c}</span>
142 ))}
143 <div className={layoutStyles.buttonRow}>
144 <StyledButton variant="primary">Details</StyledButton>
145 <StyledButton variant="ghost">Report</StyledButton>
146 {mission.status === 'critical' && (
147 <StyledButton variant="danger">Abort</StyledButton>
148 )}
149 </div>
150 </div>
151 )}
152 </div>
153 ))}
154 </div>
155 </main>
156 </div>
157 </ThemeProvider>
158 );
159}Spotted a mistake in this lesson?