02-ui-system
Chapter 2: Reusable UI System (Shadcn/ui)
In the previous chapter, we explored the "what" of our application—the EI Content Model that organizes our educational content into Modules, Lessons, and Assessments. We saw how we display a list of modules using a <Card> component.
But where did that <Card> come from? How do we make sure all the buttons, cards, and popups in our app look and feel the same?
This is where our Reusable UI System comes in. Think of it as our app's toolkit of visual Lego blocks.
The Problem: Reinventing the Wheel
Imagine building a user interface without a toolkit. First, you need a button. You'd write some HTML and CSS to make it look nice. Later, you need another button on a different page. You'd probably copy and paste the code, but maybe you make a small mistake, and now it's a slightly different shade of blue. Now imagine doing this for every single visual element: cards, input fields, dropdown menus, and dialog boxes.
Your app would quickly become a mess of inconsistent styles, and you'd waste a huge amount of time building things that have been built a million times before.
Our Reusable UI System, built using a fantastic tool called Shadcn/ui, solves this problem.
Our Box of UI Legos
Instead of building UI from scratch, we use a set of pre-built, consistently styled components. These are our "visual Lego blocks." They all live in a single, dedicated folder:
client/src/components/ui
This folder is our Lego box. Inside, you'll find files like button.tsx, card.tsx, and dialog.tsx. Each file defines one specific, reusable piece of our user interface.
When we used <Card> in Chapter 1, we were simply reaching into this box, grabbing the "Card" block, and putting it on the page.
// client/src/pages/lessons.tsx // We import the "Lego blocks" we need import { Card, CardTitle } from "@/components/ui/card"; // ... later in the code ... <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {modules.map((module) => ( // And here we use the Card block! <Card key={module.id}> <CardTitle>{module.title}</CardTitle> {/* ... other content ... */} </Card> ))} </div>
By always using the Card component from our UI toolkit, we guarantee that every card across the entire application looks exactly the same. This makes development faster and keeps our app looking clean and professional.
How to Use Our UI System: Building a Dialog Box
Let's see how easy it is to use these blocks to build something new. A common task is to ask the user for confirmation before they perform an important action. We'll build a confirmation dialog box.
-
Import the Pieces: First, we import the necessary blocks from our UI toolkit. We need a
Dialogto manage the pop-up, aDialogTriggerto be the button that opens it, and theDialogContentto hold the message.import { Button } from "@/components/ui/button"; import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog";This is like picking out the specific Lego pieces you need before you start building.
-
Assemble the Component: Now, we arrange these components in our code.
<Dialog> <DialogTrigger asChild> <Button variant="destructive">Delete Account</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Are you absolutely sure?</DialogTitle> <DialogDescription> This will permanently delete your account. </DialogDescription> </DialogHeader> </DialogContent> </Dialog>
What does this do?
<Dialog>: This is the main wrapper. It doesn't show anything itself, but it controls the state (is the dialog open or closed?).<DialogTrigger>: This component wraps the element that opens the dialog. In our case, it's a<Button>. When the user clicks this button...<DialogContent>: ...this content will appear in a pop-up window, right in the middle of the screen. Everything inside it is what the user sees.
Just by assembling these blocks, we get a fully functional, beautifully styled confirmation dialog without writing a single line of CSS.
Under the Hood: The "Recipe" Approach
You might be wondering, "How does this all work?" The magic of Shadcn/ui is that it isn't a traditional library you install. Instead, it gives you the source code for each component.
Think of it like this:
- A traditional library (like Bootstrap) is like buying a pre-built Lego car. You can use it, but you can't easily change its color or swap the wheels.
- Shadcn/ui is like getting the instruction manual and all the pieces to build the Lego car yourself. Because you have the instructions, you can change the color from red to blue or add a spoiler if you want!
This means every component in client/src/components/ui is our code. We can modify it to fit our exact needs.
Let's see how this "instruction manual" works for our <Button> component.
The component file (button.tsx) contains a "recipe" that defines all the possible styles.
// client/src/components/ui/button.tsx // This is the "recipe" using a helper called `cva` const buttonVariants = cva( // ... base styles for all buttons ... { variants: { variant: { default: "bg-primary text-primary-foreground ...", // A blue button destructive: "bg-destructive text-destructive-foreground ...", // A red button outline: "border border-input ...", // A button with an outline }, // ... }, } );
This buttonVariants function is a style mixer. When you write <Button variant="destructive">, the component uses this function to look up the correct CSS classes (bg-destructive...) and applies them to the final HTML button. This gives us incredible flexibility while ensuring perfect consistency.
This powerful UI system is the foundation of our entire front-end. It allows us to build complex interfaces for things like the Interactive Learning Exercises and to manage data fetched from the server with Server State Management (React Query).
Conclusion
The Reusable UI System is our secret weapon for building a beautiful and consistent application quickly. By providing a "Lego box" of pre-built components in client/src/components/ui, we avoid reinventing the wheel and ensure a high-quality user experience. The key takeaway is that these components aren't black boxes; they are recipes that we own and can customize to our heart's content.
Now that we understand how our app is structured and styled, let's dive into one of its most critical features: how we measure a user's knowledge.
In the next chapter, we'll explore the sophisticated engine that powers our tests: the Assessment & Scoring Engine.
Documented by [Jonny Scott]