Modals are everywhere: confirmations, forms, alerts, onboarding—you name it. Most developers reach straight for JavaScript-heavy libraries, but for many use cases, pure CSS (with a bit of SCSS power) is more than enough.
In this tutorial, you’ll build a fully reusable, accessible modal component using only CSS and SCSS—no JavaScript required. It’ll be lightweight, customizable, and easy to drop into any project.
What You’ll Learn
By the end of this tutorial, you will:
-
Understand how CSS-only modals work
-
Build a modal using the
:targettechnique -
Enhance styling and structure with SCSS
-
Add smooth open/close animations
-
Create reusable SCSS variables and mixins
-
Improve accessibility and UX without JavaScript
Who This Tutorial Is For
This guide is perfect if you are:
-
A frontend developer who wants lightweight UI components
-
Learning CSS/SCSS and want a real-world example
-
Building static sites, landing pages, or docs without JS frameworks
-
Optimizing performance by reducing JavaScript usage
Basic knowledge of HTML and CSS is enough. SCSS knowledge helps, but I’ll keep things beginner-friendly.
Final Component Features
Here’s what our modal will support:
-
Full-screen overlay backdrop
-
Centered modal dialog
-
Close button (CSS-only)
-
Smooth fade & scale animation
-
SCSS variables for colors, spacing, and z-index
-
Easy reuse across pages
Project Structure
We’ll keep things simple and clean:
css-modal/
├── index.html
├── scss/
│ └── modal.scss
└── css/
└── modal.css
We’ll write SCSS, then compile it into regular CSS.
HTML Markup for a CSS-Only Modal
Before touching CSS or SCSS, we need the right HTML structure. Since we’re not using JavaScript, the markup is everything. We’ll rely on the CSS :target pseudo-class to control when the modal is visible.
How the :target Trick Works (Quick Idea)
-
Clicking a link like
href="#modal"updates the URL fragment -
The element with
id="modal"becomes the target -
CSS can show/hide the modal based on
:target
Clean, simple, and browser-native 👌
Basic Modal HTML Structure
Create an index.html file and add the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pure CSS Modal</title>
<link rel="stylesheet" href="css/modal.css" />
</head>
<body>
<!-- Open Modal Button -->
<a href="#modal" class="btn-open">Open Modal</a>
<!-- Modal Overlay -->
<div id="modal" class="modal">
<div class="modal__content">
<a href="#" class="modal__close">×</a>
<h2>Modal Title</h2>
<p>
This is a custom modal built using pure CSS and SCSS.
No JavaScript required.
</p>
</div>
</div>
</body>
</html>
Key Elements Explained
| Element | Purpose |
|---|---|
href="#modal" |
Triggers the modal using URL targeting |
#modal |
The modal container and target |
.modal__content |
The centered dialog box |
href="#" |
Removes the target and closes the modal |
× |
Visual close icon |
Why This Structure Works Well
-
No JavaScript dependency
-
Clear separation of overlay vs content
-
Easy to style and animate
-
Works in all modern browsers
-
Degrades gracefully
Accessibility Notes (Important!)
Even without JavaScript, we’re already doing a few good things:
-
Semantic HTML (
h2,p) -
Clear close button
-
Keyboard-accessible links
Later, we’ll improve focus and visual clarity with CSS.
Base Modal Styling with CSS
In this section, we’ll:
-
Hide the modal by default
-
Create the full-screen overlay
-
Center the modal content
-
Make it visible when targeted
No animations yet—just solid, clean structure.
Base CSS (Compiled from SCSS Later)
Create css/modal.css and add the following:
/* Reset-ish */
* {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
padding: 2rem;
}
/* Open button */
.btn-open {
display: inline-block;
padding: 0.75rem 1.25rem;
background: #4f46e5;
color: #fff;
text-decoration: none;
border-radius: 6px;
}
/* Modal overlay */
.modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.3s ease;
}
/* Show modal when targeted */
.modal:target {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
/* Modal box */
.modal__content {
background: #fff;
padding: 2rem;
border-radius: 10px;
width: 100%;
max-width: 420px;
position: relative;
}
/* Close button */
.modal__close {
position: absolute;
top: 1rem;
right: 1rem;
text-decoration: none;
font-size: 1.5rem;
color: #555;
}
What’s Happening Here
1. Modal Is Hidden by Default
opacity: 0;
visibility: hidden;
pointer-events: none;
This ensures:
-
The modal isn’t visible
-
It doesn’t block clicks
-
Screen readers won’t interact with it
2. :target Makes It Appear
.modal:target {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
When the URL becomes #modal, CSS flips the switch ✨
3. Centering Without Hacks
We use Flexbox:
display: flex;
align-items: center;
justify-content: center;
Clean and modern—no transforms needed.
Quick Visual Check
At this point, you should have:
-
A button that opens the modal
-
A dark overlay covering the screen
-
A centered white dialog
-
A clickable close button
If that works, you’re golden 👍
Refactoring to SCSS (Variables & Nesting)
Our CSS works, but it’s not maintainable yet. Let’s refactor everything into SCSS so the modal is:
-
Easier to theme
-
Easier to reuse
-
Easier to extend later
We’ll use variables, nesting, and a clean component structure.
SCSS Setup
Create a new file:
scss/modal.scss
This SCSS will compile into css/modal.css.
SCSS Variables
Start with variables at the top of modal.scss:
// Colors
$overlay-bg: rgba(0, 0, 0, 0.6);
$modal-bg: #ffffff;
$primary: #4f46e5;
$text-dark: #333;
// Sizes
$radius: 10px;
$modal-width: 420px;
// Transitions
$transition: 0.3s ease;
These variables make theming ridiculously easy later 🎨
Refactored SCSS Modal Component
* {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
padding: 2rem;
}
.btn-open {
display: inline-block;
padding: 0.75rem 1.25rem;
background: $primary;
color: #fff;
text-decoration: none;
border-radius: 6px;
}
.modal {
position: fixed;
inset: 0;
background: $overlay-bg;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity $transition;
&:target {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
&__content {
background: $modal-bg;
padding: 2rem;
border-radius: $radius;
width: 100%;
max-width: $modal-width;
position: relative;
color: $text-dark;
}
&__close {
position: absolute;
top: 1rem;
right: 1rem;
font-size: 1.5rem;
text-decoration: none;
color: #666;
&:hover {
color: $primary;
}
}
}
Why This SCSS Structure Works
✅ Component-Based
Everything related to the modal lives inside .modal.
✅ BEM-Friendly
We’re using:
-
.modal -
.modal__content -
.modal__close
Easy to extend without conflicts.
✅ Easy Customization
Want a dark modal?
$modal-bg: #1f2937;
$text-dark: #f9fafb;
Boom—done.
Compile SCSS to CSS
Use any SCSS compiler:
sass scss/modal.scss css/modal.css
Or watch mode during development:
sass --watch scss/modal.scss:css/modal.css
Adding Animations (Fade & Scale Effects)
Right now, the modal works, but it pops in a bit stiff. We’ll add:
-
A subtle fade-in overlay
-
A scale + slide animation for the modal box
-
Smooth, professional transitions
Still pure CSS. Still no JavaScript.
Animation Strategy
We’ll animate two things separately:
-
Overlay → opacity
-
Modal content → transform + opacity
This keeps animations clean and predictable.
Update the SCSS Animations
Open scss/modal.scss and update the modal styles like this 👇
1️⃣ Animate the Modal Overlay
.modal {
position: fixed;
inset: 0;
background: $overlay-bg;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity $transition;
&:target {
opacity: 1;
visibility: visible;
pointer-events: auto;
.modal__content {
transform: translateY(0) scale(1);
opacity: 1;
}
}
2️⃣ Animate the Modal Content
Add animation defaults to .modal__content:
&__content {
background: $modal-bg;
padding: 2rem;
border-radius: $radius;
width: 100%;
max-width: $modal-width;
position: relative;
color: $text-dark;
transform: translateY(-20px) scale(0.95);
opacity: 0;
transition:
transform $transition,
opacity $transition;
}
How This Animation Works
| State | Overlay | Modal Content |
|---|---|---|
| Default | Hidden | Slightly up + smaller |
:target |
Fade in | Slide down + scale up |
The result feels native, not flashy 👍
Optional: Respect Reduced Motion
Accessibility win 🏆
Add this at the bottom of your SCSS:
@media (prefers-reduced-motion: reduce) {
.modal,
.modal__content {
transition: none;
}
}
This disables animations for users who prefer minimal motion.
Recompile SCSS
Don’t forget:
sass scss/modal.scss css/modal.css
Visual Result
You should now see:
-
Smooth overlay fade-in
-
Modal gently sliding and scaling into place
-
No flicker
-
No JavaScript
Improving Accessibility & UX (CSS-Only Techniques)
Even without JavaScript, we can still improve:
-
Keyboard usability
-
Visual focus clarity
-
Readability and spacing
-
User intent cues
No hacks—just thoughtful CSS.
1️⃣ Improve Focus Visibility
Links are doing the heavy lifting here, so let’s make focus states obvious.
Add this to your SCSS:
a:focus-visible {
outline: 3px solid $primary;
outline-offset: 3px;
}
This helps keyboard users instantly see where they are.
2️⃣ Make the Close Button Easier to Hit
Small targets are frustrating. Let’s improve that.
Update .modal__close:
&__close {
position: absolute;
top: 1rem;
right: 1rem;
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
text-decoration: none;
color: #666;
border-radius: 50%;
&:hover,
&:focus-visible {
background: rgba(0, 0, 0, 0.05);
color: $primary;
}
}
3️⃣ Improve Text Readability
Add spacing and line height for comfort:
.modal__content {
line-height: 1.6;
h2 {
margin-top: 0;
}
p {
margin-bottom: 0;
}
}
This makes longer modal content feel less cramped.
4️⃣ Prevent Accidental Background Interaction
We already blocked pointer events—but visually, we can also signal “modal mode”.
Add a subtle blur (optional but slick):
.modal:target {
backdrop-filter: blur(2px);
}
⚠️
backdrop-filteris optional and gracefully ignored by unsupported browsers.
5️⃣ Keyboard-Friendly Close Behavior (CSS-Only Reality Check)
With pure CSS:
-
✅ Open via keyboard
-
✅ Close via keyboard
-
❌ Trap focus inside modal
That last one requires JavaScript. This is an important, honest limitation—good to mention in the tutorial.
💡 Tip for readers:
For complex dialogs or forms, use JavaScript to manage focus trapping.
Accessibility Summary
What we can do with CSS only:
-
Clear focus styles
-
Large click targets
-
Motion preferences respected
-
Semantic content
-
Keyboard-open/close
And we’re being transparent about the limits 👍
Making the Modal Reusable (Variants & Modifiers)
A good UI component isn’t just pretty—it’s flexible. In this section, we’ll:
-
Add size variants (
sm,lg) -
Support different modal types (info, danger)
-
Keep everything clean with SCSS modifiers
Still zero JavaScript. Still lightweight.
1️⃣ Size Variants with SCSS Modifiers
We’ll extend the modal using BEM-style modifiers.
Update SCSS Variables
At the top of modal.scss, add:
$modal-sm: 320px;
$modal-lg: 640px;
Add Size Modifiers
Extend .modal__content:
.modal {
// existing styles...
&__content {
max-width: $modal-width;
&--sm {
max-width: $modal-sm;
}
&--lg {
max-width: $modal-lg;
}
}
}
Use It in HTML
<div id="modal" class="modal">
<div class="modal__content modal__content--lg">
<a href="#" class="modal__close">×</a>
<h2>Large Modal</h2>
<p>This modal uses the large variant.</p>
</div>
</div>
2️⃣ Modal Variants (Info / Danger)
Let’s support contextual modals like alerts or confirmations.
SCSS Variant Colors
Add:
$info: #2563eb;
$danger: #dc2626;
Add Variant Modifiers
.modal__content {
border-top: 6px solid $primary;
&--info {
border-color: $info;
}
&--danger {
border-color: $danger;
}
}
HTML Example
<div class="modal__content modal__content--sm modal__content--danger">
<a href="#" class="modal__close">×</a>
<h2>Delete Item</h2>
<p>This action cannot be undone.</p>
</div>
Instant visual context, zero extra CSS 🎯
3️⃣ Multiple Modals on One Page
Yes—this works beautifully.
<a href="#info-modal">Info</a>
<a href="#danger-modal">Delete</a>
<div id="info-modal" class="modal">...</div>
<div id="danger-modal" class="modal">...</div>
Each modal is controlled by its own id.
Why This Approach Scales
-
No duplicated CSS
-
Predictable naming
-
Easy overrides
-
Works in static sites, docs, and CMS pages
Perfect for blogs like Djamware that value performance ⚡
Limitations, When to Use JavaScript, and Best Practices
Pure CSS modals are awesome—but they’re not magic. Knowing when to use them (and when not to) is what separates a neat trick from solid architecture.
Limitations of CSS-Only Modals
Be upfront with readers—this builds trust.
❌ No Focus Trapping
Keyboard focus can move outside the modal. This cannot be solved with CSS alone.
❌ URL Fragment Changes
Using :target updates the URL (#modal).
Usually fine—but not ideal for:
-
SPAs
-
Analytics-sensitive pages
-
Deep-link-heavy apps
❌ No Dynamic Control
You can’t:
-
Open modals conditionally
-
Validate before close
-
Load async content
That’s JavaScript territory.
When CSS-Only Modals Are a Great Fit
Use this approach for:
-
Static websites
-
Documentation sites
-
Marketing pages
-
Simple alerts and info dialogs
-
Blog demos and tutorials
Basically: content-first modals, not app logic.
When You Should Use JavaScript Instead
Reach for JS when you need:
-
Focus trapping
-
ESC key handling
-
Dynamic content
-
Forms with validation
-
Nested or stacked modals
In those cases, this CSS modal still makes a great visual base.
Best Practices Recap
✔ Keep modal content short
✔ Always provide a clear close action
✔ Respect reduced motion
✔ Use size and type variants sparingly
✔ Be honest about limitations in docs
Final Result
You’ve built a:
-
Fully reusable modal component
-
Powered by pure CSS + SCSS
-
Accessible within CSS limits
-
Performant and dependency-free
Perfect for modern, lightweight web projects 🚀
Conclusion & Next Steps
You now have a solid, production-ready CSS-only modal. From here, you can:
-
Add JavaScript for focus trapping
-
Convert it into a Web Component
-
Integrate it into a design system
-
Theme it with CSS variables
You can find the full source code on our GitHub.
We know that building beautifully designed Mobile and Web Apps from scratch can be frustrating and very time-consuming. Check Envato unlimited downloads and save development and design time.
That's just the basics. If you need more deep learning about CSS, SASS, SCSS, TailwindCSS, you can take the following cheap course:
- CSS - The Complete Guide 2025 (incl. Flexbox, Grid & Sass)
- Advanced CSS and Sass: Flexbox, Grid, Animations and More!
- Creative Advanced CSS & JavaScript Animations - 150 Projects
- CSS Layouts Masterclass: Build Responsive-Adaptive Websites
- The Complete Sass & SCSS Course: From Beginner to Advanced
- SASS - The Complete SASS Course (CSS Preprocessor)
- The Modern Flexbox, Grid, Sass & Animations Developer Course
- Tailwind CSS From Scratch | Learn By Building Projects
- Tailwind CSS v4 - Beginner to Pro
- Tailwind CSS – The Practical Bootcamp in 2025
Thanks!
