Overview
The popup component creates a full-screen modal overlay for displaying important content like terms and conditions or detailed information. It features smooth scale animations, multi-column text layout, and uses the CSS :target pseudo-class for show/hide functionality without JavaScript.
Visual Behavior
Default State
Popup is invisible (opacity: 0, visibility: hidden)
Content is scaled down (scale(.25))
No JavaScript required
Trigger
User clicks a link with href="#popup"
URL hash changes to #popup
:target pseudo-class activates
Open State
Backdrop fades in
Content scales up from 0.25 to 1
Smooth staggered animation
Close
User clicks close button or backdrop
Link points to href="#section-tours" (different hash)
Popup fades out and scales down
The :target pseudo-class matches when the element’s ID matches the URL hash. This provides stateful behavior without JavaScript.
< div class = "popup" id = "popup" >
< div class = "popup__content" >
< div class = "popup__left" >
< img src = "img/nat-8.jpg" alt = "Tour landscape" class = "popup__img" >
< img src = "img/nat-9.jpg" alt = "Tour landscape" class = "popup__img" >
</ div >
< div class = "popup__right" >
< a href = "#section-tours" class = "popup__close" > × </ a >
< h2 class = "heading-secondary u-margin-bottom-small" >
Start booking now
</ h2 >
< h3 class = "heading-tertiary u-margin-bottom-small" >
Important – Please read these terms before booking
</ h3 >
< p class = "popup__text" >
Lorem ipsum dolor sit amet...
</ p >
< a href = "#" class = "btn btn--green" > Book now </ a >
</ div >
</ div >
</ div >
The full-screen overlay backdrop:
sass/components/_popup.scss:4-14
.popup {
height : 100 vh ;
width : 100 % ;
position : fixed ;
top : 0 ;
left : 0 ;
background-color : rgba ( $color-black , .8 );
z-index : 9999 ;
opacity : 0 ;
visibility : hidden ;
transition : all .3 s ease ;
}
Container Properties
Positioning
Backdrop
Hidden State
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100%;
Fixed positioning covers the entire viewport, staying visible during scroll. background-color: rgba( $color-black , .8 );
z-index: 9999;
Semi-transparent black background (80% opacity) creates focus on content. Extremely high z-index ensures it appears above everything. opacity: 0;
visibility: hidden;
transition: all .3s ease;
Both opacity and visibility are needed:
opacity: 0 makes it invisible
visibility: hidden prevents interaction
Smooth 0.3s fade transition
Why both opacity and visibility? opacity: 0 alone keeps the element interactive (clickable). visibility: hidden removes it from the interaction tree, preventing accidental clicks when closed.
The white content box that scales in:
sass/components/_popup.scss:16-28
& __content {
@include absCenter ;
width : 75 % ;
background-color : $color-white ;
box-shadow : 0 2 rem 4 rem rgba ( $color-black , .2 );
border-radius : 4 px ;
overflow : hidden ;
display : table ;
opacity : 0 ;
transform : translate ( -50 % , -50 % ) scale ( .25 );
transition : all .5 s .2 s ease ;
}
Content Layout
Centering:
@include absCenter ;
// Expands to:
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
Perfectly centers the content box in the viewport.
Dimensions:
width: 75%; // Responsive width
height: auto; // Height adapts to content
Display:
display: table; // Enables table-cell children
Using CSS table layout allows the left/right columns to behave like table cells with automatic equal height.
opacity: 0;
transform: translate(-50%, -50%) scale( .25 );
Understanding the Transform
Staggered Animation
transition: all .5s .2s ease;
Duration: 0.5s (longer than backdrop)
Delay: 0.2s (starts after backdrop begins)
Result: Backdrop fades in first, then content scales up
The staggered timing creates a more polished, professional feel. The content doesn’t appear until the backdrop is partially visible.
Content Columns
Left Column (Images)
sass/components/_popup.scss:30-33
& __left {
width : 33.333333 % ;
display : table-cell ;
}
Takes up 1/3 of the content width. Using table-cell display ensures it matches the height of the right column.
Right Column (Text)
sass/components/_popup.scss:35-40
& __right {
width : 66.6666667 % ;
display : table-cell ;
vertical-align : middle ;
padding : 3 rem 5 rem ;
}
Layout:
Takes up 2/3 of content width
vertical-align: middle centers content vertically
Generous padding (3rem top/bottom, 5rem sides)
sass/components/_popup.scss:42-45
& __img {
display : block ;
width : 100 % ;
}
Images stack vertically, each taking full width of the left column.
HTML Usage
< div class = "popup__left" >
< img src = "img/nat-8.jpg" alt = "Tour landscape" class = "popup__img" >
< img src = "img/nat-9.jpg" alt = "Tour landscape" class = "popup__img" >
</ div >
Two images displayed one above the other.
Multi-Column Text
The text content uses CSS columns for newspaper-style layout:
sass/components/_popup.scss:47-63
& __text {
font-size : 1.4 rem ;
margin-bottom : 4 rem ;
-moz-column-count : 2 ;
-moz-column-gap : 4 rem ;
-moz-column-rule : 1 px solid $color-grey-light-2 ;
column-count : 2 ;
column-gap : 4 rem ;
column-rule : 1 px solid $color-grey-light-2 ;
-webkit-hyphens : auto ;
-moz-hyphens : auto ;
-ms-hyphens : auto ;
hyphens : auto ;
}
Column Properties
Column Count
Column Gap
Column Rule
Hyphenation
Hyphenation (hyphens: auto) automatically breaks long words across lines with hyphens, preventing awkward spacing in narrow columns. Requires the lang attribute on the HTML element to work properly.
Vendor Prefixes
CSS columns require prefixes for full browser support:
-moz-column-count: 2; // Firefox
-moz-column-gap: 4rem; // Firefox
-moz-column-rule: 1px solid $color-grey-light-2 ; // Firefox
column-count: 2; // Standard
column-gap: 4rem; // Standard
column-rule: 1px solid $color-grey-light-2 ; // Standard
Open State (:target)
When the popup’s ID matches the URL hash:
sass/components/_popup.scss:66-74
& :target {
opacity : 1 ;
visibility : visible ;
}
& :target & __content {
opacity : 1 ;
transform : translate ( -50 % , -50 % ) scale ( 1 );
}
Backdrop Visible
& :target {
opacity : 1 ;
visibility : visible ;
}
When URL is page.html#popup, the backdrop fades in and becomes interactive.
Content Scales In
& :target & __content {
opacity : 1 ;
transform : translate ( -50 % , -50 % ) scale ( 1 );
}
Content fades in and scales from 0.25 to 1 (full size) while maintaining center position.
The X button in the top-right corner:
sass/components/_popup.scss:76-93
& __close {
& :link ,
& :visited {
color : $color-grey-dark-1 ;
position : absolute ;
top : .8 rem ;
right : 2.5 rem ;
font-size : 3 rem ;
text-decoration : none ;
display : inline-block ;
transition : all .2 s ease ;
}
& :hover {
color : $color-primary ;
transform : scale ( 1.3 );
}
}
Content:
< a href = "#section-tours" class = "popup__close" > × </ a >
The × HTML entity displays as the × symbol.
Positioning:
position: absolute;
top: .8rem ;
right: 2 .5rem ;
Positioned in the top-right corner of the content box.
Hover Effect:
color: $color-primary ; // Grey to green
transform: scale(1 .3 ); // Grows 30%
Button grows and changes color on hover, making it clear it’s interactive.
How Closing Works
< a href = "#section-tours" class = "popup__close" > × </ a >
Clicking changes the URL hash from #popup to #section-tours, which:
Removes the :target match from .popup
Triggers the reverse transition
Popup fades out and scales down
Page scrolls to the tours section
You can also close by clicking the backdrop. Add a click handler to the .popup container that changes the location hash.
Any link can open the popup by targeting its ID:
< a href = "#popup" class = "btn btn--white" > Book now </ a >
From Text Links
< a href = "#popup" > Read terms and conditions </ a >
< button onclick = " window . location . hash = 'popup'" >
Open popup
</ button >
Real-World Example
Complete popup from the Natours booking flow:
< div class = "popup" id = "popup" >
< div class = "popup__content" >
< div class = "popup__left" >
< img src = "img/nat-8.jpg" alt = "Tour landscape" class = "popup__img" >
< img src = "img/nat-9.jpg" alt = "Tour landscape" class = "popup__img" >
</ div >
< div class = "popup__right" >
< a href = "#section-tours" class = "popup__close" > × </ a >
< h2 class = "heading-secondary u-margin-bottom-small" >
Start booking now
</ h2 >
< h3 class = "heading-tertiary u-margin-bottom-small" >
Important – Please read these terms before booking
</ h3 >
< p class = "popup__text" >
Lorem ipsum dolor sit amet. Qui quidem sapiente vel eius iste
eum accusamus corporis eos dolore illum...
</ p >
< a href = "#" class = "btn btn--green" > Book now </ a >
</ div >
</ div >
</ div >
Triggered by: Card “Book now” buttons
Purpose: Display terms and conditions before booking
Actions: Close (return to tours) or Book now
Animation Timeline
t = 0ms: User Clicks
Link with href="#popup" is clicked URL changes to include #popup hash
t = 0-300ms: Backdrop Fades In
.popup {
opacity : 0 → 1
visibility : hidden → visible
transition : all .3 s
}
t = 200ms: Content Starts
Content animation begins (0.2s delay)
t = 200-700ms: Content Scales In
.popup__content {
opacity : 0 → 1
scale : .25 → 1
transition : all .5 s .2 s
}
t = 700ms: Animation Complete
Popup fully visible and interactive
Browser Compatibility
:target Pseudo-class
The :target pseudo-class has excellent browser support:
Chrome/Edge: Yes
Firefox: Yes
Safari: Yes
IE: 9+
No fallback needed for modern browsers.
CSS Columns
CSS multi-column layout requires vendor prefixes: -moz-column-count: 2; // Firefox
column-count: 2; // Standard
Most modern browsers support the standard syntax, but prefixes ensure compatibility with older versions.
CSS Hyphens
-webkit-hyphens: auto; // Safari
-moz-hyphens: auto; // Firefox
-ms-hyphens: auto; // IE/Edge
hyphens: auto; // Standard
Browser support: Good with prefixes. Requires lang attribute on HTML element.
Accessibility Considerations
Keyboard Navigation
Add focus trap to keep keyboard navigation within the popup:
const popup = document . querySelector ( '.popup' );
const closeBtn = popup . querySelector ( '.popup__close' );
// Focus close button when popup opens
if ( window . location . hash === '#popup' ) {
closeBtn . focus ();
}
// Close on Escape key
document . addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Escape' && window . location . hash === '#popup' ) {
window . location . hash = '' ;
}
});
ARIA Attributes
Improve screen reader support:
< div class = "popup" id = "popup" role = "dialog"
aria-labelledby = "popup-title" aria-modal = "true" >
< div class = "popup__content" >
< div class = "popup__left" >
< img src = "img/nat-8.jpg" alt = "Scenic mountain landscape"
class = "popup__img" >
< img src = "img/nat-9.jpg" alt = "Forest hiking trail"
class = "popup__img" >
</ div >
< div class = "popup__right" >
< a href = "#section-tours" class = "popup__close"
aria-label = "Close popup" > × </ a >
< h2 class = "heading-secondary u-margin-bottom-small"
id = "popup-title" >
Start booking now
</ h2 >
<!-- content -->
</ div >
</ div >
</ div >
Focus Management
Best practice: When the popup opens, move focus to the close button or first interactive element. When it closes, return focus to the element that opened it.
JavaScript Enhancements
Close on Backdrop Click
const popup = document . querySelector ( '.popup' );
popup . addEventListener ( 'click' , function ( e ) {
// Close if clicking the backdrop (not the content)
if ( e . target === popup ) {
window . location . hash = '' ;
}
});
Prevent Body Scroll
// Disable body scroll when popup is open
function toggleBodyScroll () {
if ( window . location . hash === '#popup' ) {
document . body . style . overflow = 'hidden' ;
} else {
document . body . style . overflow = '' ;
}
}
window . addEventListener ( 'hashchange' , toggleBodyScroll );
toggleBodyScroll (); // Check on page load
Customization Examples
Changing Animation Speed
.popup {
transition : all .5 s ease ; // Slower backdrop
}
.popup__content {
transition : all .8 s .3 s ease ; // Slower content, longer delay
}
Different Scale Effect
.popup__content {
// Slide in from top instead of scale
transform : translate ( -50 % , -100 % ) scale ( 1 );
}
.popup:target .popup__content {
transform : translate ( -50 % , -50 % ) scale ( 1 );
}
Wider Content
.popup__content {
width : 90 % ; // Wider popup
max-width : 1200 px ; // But not too wide
}
Single Column Text
.popup__text {
column-count : 1 ; // No columns
// or remove column properties entirely
}
GPU Acceleration: The transform property (scale and translate) triggers hardware acceleration for smooth animations.Efficient properties:
transform - GPU accelerated
opacity - GPU accelerated
No layout recalculation required
Optimization:
.popup__content {
will-change : transform , opacity ; // Hint to browser
}
.popup:target .popup__content {
will-change : auto ; // Release after animation
}
Don’t overuse will-change - only apply it to elements that will definitely animate, and remove it afterward to free GPU memory.
Common Use Cases
Terms & Conditions
Image Gallery
Video Player
Form
Display legal text before user completes an action < a href = "#terms-popup" class = "btn" > Review Terms </ a >
Show full-size image with caption < a href = "#image-1-popup" >
< img src = "thumb.jpg" alt = "Thumbnail" >
</ a >
Embed video in centered modal < a href = "#video-popup" class = "play-button" >
Watch Demo
</ a >
Display contact or registration form < a href = "#contact-popup" class = "btn" >
Contact Us
</ a >
Summary
The popup component demonstrates:
:target pseudo-class for stateful CSS without JavaScript
Staggered animations for polished transitions
Transform scale for smooth entrance/exit effects
CSS columns for advanced text layout
Table layout for equal-height columns
Absolute centering with transform
High z-index for overlay stacking
Opacity + visibility for proper hiding
BEM methodology for organized structure
This component shows how CSS alone can create sophisticated UI patterns traditionally requiring JavaScript, with the option to enhance with JS for improved accessibility and UX.