The Problem
Creating a platform for personalized proposals sounds straightforward until you realize what's actually at stake. It's not just a website. It's a moment. Someone's asking the most vulnerable question they can ask another person. They deserve more than a generic form. They deserve something that feels like it understands the weight of what they're doing.
The project needed multiple pieces working together seamlessly. A landing page that explains what LovedIn is and gets people excited about it. A stories page where people can share their proposals and celebrate with others. And then the proposal page itself, where the magic happens.
Who's going to use this? People aged 18 to 35 who want their proposals to be memorable. They're on their phones, they're probably nervous, and they want something that works smoothly without any hiccups.
The goal was simple but ambitious: create a complete platform that feels personal, that celebrates what's actually happening. Not generic or boring. Something that makes you feel the weight of the moment while also making it feel safe enough to be playful about it. A platform where someone could create a customized proposal experience and then share it with the person they love.
How I Approached It
I worked on three main parts of this project: the proposal page, the login page, and the signup page. Each one had its own challenge, but they all had to feel like they came from the same place, the same design language.
For the proposal page, I designed it to have two distinct states visually. First, there's the question state where you see the personalized proposal. Then there's the success state that celebrates the "yes" moment. Both are built with HTML and CSS, structured so they can be toggled with a class when JavaScript gets added later. The structure made sense because it keeps everything on one page. The visual hierarchy and layout shift from one state to another through CSS class management. It's designed to be clean and cohesive.
I added animated GIFs to break up the formality and set the emotional tone. The proposal state shows a cute animated character peeking out nervously, like someone getting ready to ask the big question. It's the feeling before you ask. The success state shows a baby cheering, celebrating the "yes". These aren't just random images thrown in. They set the tone. They make it feel like the page understands that this is emotional, vulnerable, and deserves celebration.
The GIFs are styled to be prominent but not overwhelming. They're centered, have a border in the accent color, and cast a subtle shadow. They draw your eye but they don't scream for attention.
I also designed the "No" button to do something playful. When you hover over it, it moves away. This is intentional. It's playful. It acknowledges the nervousness of the moment but keeps things light. The button doesn't disappear completely, it just shifts position. On desktop, it moves more dramatically, 80 pixels to the right and 30 pixels up. On mobile, it moves less because the screen is smaller and you don't want the button flying off screen entirely. It's 40 pixels right and 15 pixels up on smaller devices.
This movement is pure CSS using transform and transition. No JavaScript needed. The transform property moves the button without affecting the layout, which keeps everything smooth and performant. The transition makes it feel responsive and snappy.
For the login and signup pages, the approach was different but connected. I structured them with proper semantic HTML so they were accessible, then styled them to feel warm and inviting using the same design system. The forms had to be straightforward but not sterile. A person coming to log in or sign up is already taking a step toward something, so the interface should meet them with ease, not friction.
Both the proposal page and the auth pages had to work together in the same design language. That's where the design system became crucial.
The Design System
Colors matter more than people think, especially when you're trying to create an emotional experience. I worked with five specific colors from our design system, and each one has a purpose and a name.
Scarlet Blush is #E22B3B, the primary color. It's bold, it's romantic, and it demands attention. You see it in headings and main buttons. It's confident without being aggressive. Wild Strawberry is #ED4779, the secondary color. It's softer than Scarlet Blush, more playful. It shows up in gradients and accents. It's romantic without being too serious. Petal Frost is #FA88BB, the accent color. It's tender and warm. It shows up in borders and highlights, adding softness to hard edges. Pink Carnation is #F0D1D7, the background color. It's this soft pink that makes every page feel intimate and calm. And Blush Rose is #D86A77, the support color. It grounds everything. It works for secondary text and borders, keeping things from floating away.
For fonts, I used Playfair Display for headlines. It's elegant, it's got personality, and when you see your partner's name in this font, it feels formal and important. That's intentional. Inter is what everything else uses because it's clean and readable everywhere, on every device.
The whole design system exists in base.css as CSS variables. This means we're not hardcoding colors. If we ever need to change Scarlet Blush, we change it once and it updates everywhere. That's the power of a good system. My partner built this foundation, and it made everything else possible.
The HTML Structure
I made sure the HTML was semantic and accessible because honestly, if people can't access your page or search engines can't understand it, none of the styling matters.
For the proposal page, the structure is straightforward. There's a main section that holds two states: the proposal state and the success state. Both are built in HTML, ready for JavaScript to toggle between them. The proposal state has the question, the GIFs, and the buttons. The success state has the celebration message.
For the auth pages, the structure had to be clean but also make sense. Each form is wrapped in a fieldset with a legend. The legend tells you what section you're in. The form groups are organized vertically, each with a label and input. It's simple, but it's built correctly.
Each heading gets its proper semantic tag. The main question on the proposal page is an h1, the success message is an h2. This creates a proper document outline so screen readers understand what's happening. The buttons have proper IDs so they can be targeted later with JavaScript.
Here's what the proposal page structure looks like:
<body>
<main>
<section class="proposal-page active">
<button class="back-btn">← Back</button>
<section class="proposal-container">
<div class="proposal-state" id="proposalState">
<img src="..." alt="Cute animated character peeking out nervously" class="proposal-gif" />
<h1 class="proposal-container__title">Will you be my girlfriend?</h1>
<p class="proposal-container__text">This is the moment that changes everything...</p>
<div class="proposal-buttons">
<button class="proposal-buttons__yes" id="yesBtn">Yes! 💕</button>
<button class="proposal-buttons__no" id="noBtn">No!</button>
</div>
</div>
<div class="success-state" id="successState">
<img src="..." alt="Animated baby raising their arms and cheering" class="success-gif" />
<h2 class="proposal-container__heading">You've Made Me The Happiest</h2>
<p class="proposal-container__text">Welcome to the next chapter of our story together</p>
<p class="success-message">This moment, this "yes" - I'll treasure it forever...</p>
<button class="btn btn-primary">Back Home</button>
</div>
</section>
</section>
</main>
</body>
And here's what the auth page structure looks like:
<main class="auth-page">
<section class="auth-page-text">
<h1 class="auth-text-heading">Welcome Back</h1>
<p class="auth-text-para">Log in to your LovedIn account</p>
</section>
<form class="auth-form">
<fieldset class="auth-field">
<legend class="auth-form-heading">Login Details</legend>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required placeholder="you@example.com" />
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required placeholder="Enter your password" />
</div>
<button type="submit" class="submit-btn">Login</button>
</fieldset>
</form>
<div class="auth-footer">
<p class="auth-footer-text">Don't have an account? <a href="signup.html" class="redirect-link">Sign up</a></p>
</div>
</main>
I added Open Graph meta tags so when someone shares this on Twitter or Facebook, it shows a nice preview with the right image and description. Meta tags matter for shareability, and proposals are definitely going to be shared.
The alt text on the GIFs is descriptive, not just "gif". That's for accessibility and for anyone who can't load the images for whatever reason.
SEO Optimization
Each page has a unique, descriptive title tag. The proposal page's title is "LovedIn - Your Proposal". That tells search engines exactly what this page is about. The meta description explains the page in 160 characters, which is what shows up in search results.
Open Graph tags are crucial for this project because proposals are shareable moments. When someone posts the proposal link on Facebook or Twitter, the preview shows the right image, title, and description. That's Open Graph doing its job. Without it, you'd just see a generic URL.
The heading hierarchy matters too. I didn't just throw h1, h2, h3 at things randomly. There's one h1 per page, which tells search engines what the main topic is. The h2 is for secondary content. This structure helps Google understand the page better.
The alt text on images does double duty. It helps people using screen readers understand what the images are, and it also helps search engines understand the images. Good alt text is descriptive and relevant. "Celebration GIF" is better than "image123". "Animated baby raising their arms and cheering excitedly to celebrate the positive response" is even better because it describes exactly what's happening.
Here's what the meta tags and Open Graph setup looks like in the HTML head:
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LovedIn - Your Proposal</title>
<meta name="description" content="A personalized proposal experience created with LovedIn." />
<!-- Open Graph Tags for Social Sharing -->
<meta property="og:type" content="website" />
<meta property="og:title" content="LovedIn - Your Proposal" />
<meta property="og:description" content="A personalized proposal experience created with LovedIn." />
<meta property="og:image" content="https://lovedin.vercel.app/assets/images/meta-proposal.png" />
<meta property="og:url" content="https://lovedin.vercel.app/proposal.html" />
<!-- Twitter Card Tags for Twitter Sharing -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="LovedIn - Your Proposal" />
<meta name="twitter:description" content="A personalized proposal experience created with LovedIn." />
<meta name="twitter:image" content="https://lovedin.vercel.app/assets/images/meta-proposal.png" />
<!-- Additional SEO -->
<meta name="theme-color" content="#ED4779" />
<link rel="icon" href="assets/images/favicon.ico" />
</head>
Here's what each tag does:
Basic Meta Tags:
-
<title>- Shows up in browser tab and search results. Keep it under 60 characters -
<meta name="description">- Shows under the title in search results. Around 160 characters -
<meta name="viewport">- Makes the site responsive on mobile devices
Open Graph Tags (for Facebook, LinkedIn, etc.):
-
og:title- The title shown when someone shares the link -
og:description- The description shown when someone shares the link -
og:image- The image shown when someone shares the link (this is crucial for proposals because the preview is visual) -
og:url- The URL of the page -
og:type- What type of content this is (website, article, etc.)
Twitter Card Tags (specifically for Twitter/X):
-
twitter:card- Type of card. "summary_large_image" shows a big preview with image -
twitter:title- Title for Twitter -
twitter:description- Description for Twitter -
twitter:image- Image for Twitter
The overall structure and content on the page tell a story that search engines can understand. When someone searches for "personalized proposal experience" or "romantic proposal idea", we want these pages to show up. The semantic HTML and clear content structure makes that possible.
When someone shares the proposal link on Facebook, they see the Scarlet Blush color, the proposal image, and a clear description. That's Open Graph working. It makes sharing the proposal feel special, not like a generic link.
The overall structure and content on the page tell a story that search engines can understand. When someone searches for "personalized proposal experience" or "romantic proposal idea", we want these pages to show up. The semantic HTML and clear content structure makes that possible.
The CSS Architecture
My partner built a comprehensive design system in base.css that defines everything we need: colors, spacing, typography scales, border radius values, shadows, and transition durations. Everything is a variable. I had to learn to use that system properly and extend it where the proposal and auth pages needed something different.
Here's why this matters. When I was styling the proposal page or the auth pages, I didn't write pixel values or hex codes. I wrote var(--spacing-lg) or var(--color-primary). This sounds like extra work, but it's actually the opposite. It's faster because you're following an established pattern. And if we need to change something across the entire site, we change it once.
Here's what the design system looks like:
:root {
/* Colors */
--color-primary: #e22b3b; /* Scarlet Blush */
--color-primary-bg: #f0d1d7; /* Pink Carnation */
--color-secondary: #ed4779; /* Wild Strawberry */
--color-secondary-bg: #fff6f8;
--color-accent: #fa88bb; /* Petal Frost */
--color-support: #d86a77; /* Blush Rose */
/* Typography */
--font-display: "Playfair Display", serif;
--font-body: "Inter", sans-serif;
--heading-h2: 2.25rem;
--heading-h3: 1.875rem;
/* Spacing */
--spacing-lg: 1.5rem;
--spacing-md: 1rem;
--spacing-sm: 0.5rem;
/* Shapes */
--radius-lg: 12px;
--radius-2xl: 20px;
/* Shadows */
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* Transitions */
--transition-base: 250ms ease-in-out;
}
For the proposal page, the buttons are arranged with flexbox, which is the right choice for this layout. I set display to flex, gap handles the spacing between buttons, justify-content centers them horizontally, and flex-wrap makes sure they stack on smaller screens. This is responsive by default without needing a ton of media queries.
Here's the button styling for the proposal page:
.proposal-buttons {
display: flex;
gap: var(--spacing-lg);
justify-content: center;
flex-wrap: wrap;
margin-top: var(--spacing-2xl);
}
.proposal-buttons__yes {
background: linear-gradient(
135deg,
var(--color-primary),
var(--color-secondary)
);
color: var(--color-white);
padding: var(--spacing-md) var(--spacing-2xl);
border: none;
border-radius: var(--radius-md);
font-weight: var(--font-weight-bold);
cursor: pointer;
transition: var(--transition-base);
box-shadow: 0 4px 12px rgba(226, 43, 59, 0.15);
}
.proposal-buttons__no {
background: rgba(226, 43, 59, 0.08);
color: var(--color-primary);
padding: var(--spacing-md) var(--spacing-2xl);
border: var(--border-width-medium) solid var(--color-primary);
border-radius: var(--radius-md);
cursor: pointer;
}
For the auth pages, the form styling had to feel welcoming while still being functional. Here's the form structure:
.auth-form {
background: var(--color-white);
border-radius: var(--radius-lg);
padding: var(--spacing-4xl) var(--spacing-2xl);
max-width: 400px;
width: 100%;
box-shadow: var(--shadow-lg);
border: var(--border-width-thin) solid var(--color-primary-bg);
}
.auth-field {
border: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.form-group input {
padding: var(--spacing-md) var(--spacing-md);
border: var(--border-width-medium) solid var(--color-primary-bg);
border-radius: var(--radius-md);
font-family: var(--font-body);
font-size: var(--font-size-sm);
transition: var(--transition-base);
background-color: var(--color-white);
color: var(--color-gray-900);
}
.form-group input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(226, 43, 59, 0.1);
}
I used BEM naming for all the CSS classes. Block, Element, Modifier. This means class names tell you exactly what they're for. .proposal-container is the block. .proposal-container__title is an element inside it. It's clear, it's scalable, and it prevents naming conflicts.
Here's an example of BEM structure in the proposal container:
.proposal-container {
background: var(--color-white);
border-radius: var(--radius-2xl);
padding: var(--spacing-4xl) var(--spacing-2xl);
max-width: 600px;
width: 100%;
box-shadow: var(--shadow-xl);
text-align: center;
position: relative;
}
.proposal-container__title {
font-family: var(--font-display);
font-size: var(--heading-h2);
font-weight: var(--font-weight-extrabold);
color: var(--color-gray-900);
margin-bottom: var(--spacing-sm);
}
.proposal-container__text {
font-size: var(--font-size-md);
color: var(--color-gray-500);
margin-bottom: var(--spacing-lg);
font-weight: var(--font-weight-medium);
}
I didn't use element selectors like "h1" or "button" for styling. Everything is class-based. This sounds strict, but it's actually really good practice because it decouples your CSS from your HTML. If the HTML structure changes, your CSS doesn't break. And when you're working on a team, this prevents one person's styling decisions from affecting another person's work.
The DRY Principle and Button Styles
At first, I had button styles in auth.css. Then I had them again in proposal.css. That's duplication, and it's a problem. If we need to update how buttons look, we'd have to change multiple files. That's asking for bugs.
So I centralized all button styles in base.css. Now auth pages use them, proposal pages use them, and any other page that comes along uses them. One source of truth. When we update buttons, it happens everywhere automatically.
This is called the DRY principle: Don't Repeat Yourself. It's one of those fundamentals that sounds obvious until you see how much time it saves you in the long run.
Responsive Design
The proposal page, the login page, and the signup page all need to work on phones, tablets, and desktops. The way I handled this was with media queries that adjust sizes and spacing for smaller screens.
On mobile, the container gets less padding so it fits better. The headings get smaller. Everything is appropriately sized for the device. All of this happens at a breakpoint around 640 pixels wide for the auth pages and 768 pixels for the proposal page, which is where most tablets switch to portrait orientation.
What Was Actually Hard
The biggest challenge wasn't the code. It was maintaining consistency while working with someone else on the same project. My partner was building out base.css, the homepage, and stories page while I was working on the proposal page and auth pages. We both needed to use the same button styles, the same colors, the same spacing scale.
At the beginning, I was using element selectors like "h1" or direct targeting. That works fine until your partner structures their HTML differently, and then suddenly the styling breaks or looks inconsistent. Or you both style buttons and they look slightly different because you made slightly different choices.
The Git workflow was challenging too. I had to learn how to create proper feature branches, write good commit messages, and handle merge conflicts when they happened.
Learning to use Git branches properly was key. We established a pattern where each feature got its own branch. I'd create a branch like "feature/proposal-page-styling", do my work, commit with clear messages like "refactor(css): Remove button styling duplication", push that branch, and then create a pull request for my partner to review. Only after they approved would it get merged to the main branch.
This workflow felt like extra steps at first, but it actually saved us from so many problems. And having code review from my partner meant I caught issues I might have missed.
Another challenge was getting the responsive design right. The proposal page and auth pages need to look good on phones, tablets, and desktops. Getting padding, font sizes, and button spacing to scale properly across all those sizes took careful testing and tweaking. Mobile design is harder than it looks.
The solution was establishing a clear system and sticking to it. We documented how classes should be named. We centralized shared styles in base.css. We used variables for everything. Did this take time upfront? Yeah. Was it worth it? Absolutely. Because after that, we could work independently without worrying about breaking each other's work.
What I'd Do Differently in Version 2.0
First, I'd add animations to the page transitions. Right now the states are designed to swap with classes, but in version 2 I'd add CSS animations so they fade or slide in smoothly when the state changes.
Second, I'd improve the back button styling. Currently it's functional, but I'd make it more integrated into the overall design. Maybe animate it on hover or give it more visual feedback.
Third, more focus on micro-interactions. Things like button hover effects, focus states for keyboard navigation, and smooth transitions everywhere. These small details make a huge difference in how polished something feels.
Fourth, I'd optimize the layout further for tablets. Right now I have desktop and mobile, but tablets are their own thing. Spending more time on that middle breakpoint would help.
Fifth, I'd add more sophisticated typography handling. Maybe different heading sizes for different sections, better line heights, better letter spacing. Typography is understated but incredibly important.
Sixth, I'd consider adding CSS custom properties for even more granular control over things like opacity, blur effects, and other visual effects that might make the page feel more alive.
What I Learned
Building this taught me that design systems aren't just about making things look consistent. They're about enabling teams to work together without friction. When everyone follows the same patterns and uses the same variables, the code becomes easier to understand and easier to maintain. My partner laid that foundation with base.css, and I learned to trust it and build on top of it.
I also learned that constraints are actually helpful. When my partner constantly reinforced that "no element selectors, everything has to be a class", that forced me to think more carefully about naming and structure.
I learned that CSS can do way more than people think. You don't need JavaScript for a lot of things. Flexbox, media queries, CSS variables, hover states, they're all super powerful. Learning to work within CSS constraints made me a better developer.
Finally, I learned that responsive design is an art. Getting something to look good on a 5-inch phone and a 27-inch monitor takes real thought. It's not just about making things smaller or bigger. It's about understanding how people interact with different screen sizes and designing for that.
I also learned something about working with another person on code. It requires vulnerability. You have to be willing to have your code reviewed, to hear that something doesn't work, to try a different approach. It requires patience. Sometimes the person working on the foundations (like base.css) is the unsung hero because their work enables everything else. And it requires trust. Trust that your partner is doing their part, trust that their approach is intentional, trust that they care about the outcome as much as you do.
The Live Experience
You can check out the proposal page here: https://lovedin.vercel.app/
The code is all on GitHub: https://github.com/Yourgotopyromaniac/lovedin
The design system documentation that guided all of this: https://tidy-nape-704.notion.site/LovedIn-Design-Documentation-2fcff83f0c4c80cea70bfefc37e883e0
The broader case study for the whole project: https://docs.google.com/document/d/1BwLFD08gpjclHfZflFJPF8wNfVAWR2nbD_FLrHhudEE/edit
Acknowledgment
This happened because of collaboration. My partner Awoyemi Abiola did the heavy lifting on this project. They built the base.css design system that everything else is built on top of, styled the index.html landing page, and created the stories.html page and its styling. Without that foundation, my work on the proposal page and the auth pages would have been way harder.
We had to sync up, make decisions together, and trust each other's work. That's how you build something actually good. When I was building the proposal page and auth pages, I could rely on the design system Awoyemi created. When they were styling the homepage, they could count on the system being consistent.
Check out Awoyemi's GitHub: https://github.com/Yourgotopyromaniac
And their version of this case study:




Top comments (0)