- Basilica!
Continue with the JS section on Front End Masters
- Continue working with Flexbox and introduce CSS Grids
- Review basic DOM manipulation techiques
- Understand how to create an element and insert it into the DOM
- Review GIT and Github set up and branching
- Review NPM set up and installing
Download a zip file of this repo and cd into the directory.
Initialize a new git repository:
$ git init
$ git add .
$ git commit -m 'initial commit'Note: the project is prepared for npm. It has a .gitignore file with the text node_modules in it.
Create a manifest (package.json) and install packages.
$ npm init
$ npm install browser-sync --save-devReview:
- package.json
- package-lock.json
- hard dependencies vs devDependencies
- node_modules folder
- the need for a
.gitignorefile
Browser Sync CLI documentation
Add an npm command to the scripts section of package.json:
"scripts": {
"start": "browser-sync app -w"
},In VSCode's integrated terminal:
$ npm run start
Open app/index.html in VSCode and examine the HTML with regards to the recipe schema. A schema is a semantic vocabulary of tags (or microdata) that you can add to your HTML to improve the way search engines read and represent your page in search engine result pages. There are many different kinds of schemas.
Note the itemscope attribute. There is a good example of using the recipe schema.
Demo: test the HTML with Rich Results Test and add any missing attributes.
There are other types of metadata that enrichness HTML. A popular one for sharing data is Open Graph Metadata.
Inspect the head of this recipe page and note the og: metadata.
Note the <abbr> tag.
Examine the starter CSS.
Note:
- The pseudo class
::selection - the use of
max-widthon the body selector - the
li > h4child selector used to select elements with a specific parent. In this case it will selecth4tags only when the immediate parent is anli(compare this toli h4). Here's a complete listing of selector types in CSS. - the css variables:
html {
--basil-green: #88a308;
--dark-gray: #333333;
--light-gray: #e4e1d1;
--light-green: #f5faef;
--orange: #f90;
--light-orange: #ebbd4e;
--red: #f00;
--max-width: 840px;
--radius: 4px;
}CSS variables are defined at a high level in the CSS (here the html selector is used ) to ensure that all the elements inherit and can use them.
CSS variables are applied as follows:
p {
color: var(--basil-green);
}Note also: the transition property on the anchors. transition: color 0.2s linear; is a shortcut for:
transition-property: color;
transition-duration: 1s;
transition-timing-function: linear;Responsive Images work well on devices with different screen sizes and resolutions. Add:
img {
width: 100%;
}Use width: 100% on images and videos to set the width to whatever the image's container's width is. CSS also provides control over aspect ratio which is especially useful for videos.
The problem with our responsive image as currently coded can be observed by throttling the download speed, turning off Cache in the Network tab of the dev tools and doing a hard refresh by right clicking on the browser's refresh icon.
Note the Cumulative Layout Shift which occurs.
(Be sure to turn off throttling and enable cache before continuing.)
Replace the lone img tag in the HTML with <figure> and <figcaption> tags:
<figure>
<img
src="img/pesto.jpg"
itemprop="image"
alt="a bold of pesto with a wooden spoon in it"
/>
<figcaption>
Classic, simple basil pesto recipe with fresh basil leaves, pine nuts,
garlic, Romano or Parmesan cheese, extra virgin olive oil, and salt and
pepper.
</figcaption>
</figure>The <figure> tag allows an optional <figcaption> element.
We want to display identical image content across devices but the dimensions should be larger or smaller depending on the device - e.g. a smaller image for mobile users. While the standard <img> element points the browser to a single source file, two attributes — srcset and sizes — provide additional source images along with hints to help the browser pick the right one.
srcset defines a set of images that allow the browser to choose which image size to use.
Examine image use in a typical NY Times article:
<img
alt="A man in a purple hoodie talks to a group of teenagers. "
class="css-jb88yk"
src="https://static01.nyt.com/images/2023/06/01/multimedia/00ny-no-cops90-vkgl/00ny-no-cops90-vkgl-articleLarge.jpg?quality=75&auto=webp&disable=upscale"
srcset="
https://static01.nyt.com/images/2023/06/01/multimedia/00ny-no-cops90-vkgl/00ny-no-cops90-vkgl-articleLarge.jpg?quality=75&auto=webp 600w,
https://static01.nyt.com/images/2023/06/01/multimedia/00ny-no-cops90-vkgl/00ny-no-cops90-vkgl-jumbo.jpg?quality=75&auto=webp 1024w,
https://static01.nyt.com/images/2023/06/01/multimedia/00ny-no-cops90-vkgl/00ny-no-cops90-vkgl-superJumbo.jpg?quality=75&auto=webp 2048w
"
sizes="100vw"
decoding="async"
width="600"
height="400"
/>Note the width and height attributes to prevent layout shift.
Replace the img tag in index.html with:
<img
alt="a bowl of pesto with a wooden spoon in it"
src="img/pesto.jpg"
srcset="
img/pesto/pesto_iodywc_c_scale,w_380.jpg 600w,
img/pesto/pesto_iodywc_c_scale,w_780.jpg 1024w,
img/pesto/pesto_iodywc_c_scale,w_1380.jpg 2048w
"
width="100%"
height="auto"
/>Hard reload the page at various widths and look in the Sources panel of the developer tools to see the image that was chosen by the browser.
Note: you might also see a picture tag used on occasion:
<picture>
<source
srcset="img/pesto/pesto_iodywc_c_scale,w_1380.jpg"
media="(min-width: 1200px)"
/>
<source
srcset="img/pesto/pesto_iodywc_c_scale,w_780.jpg"
media="(min-width: 800px)"
/>
<img itemprop="image" src="img/pesto.jpg" />
</picture>The <picture> tag is used for cropping or modifying images for different media conditions or offering different image formats when certain formats are not supported by all browsers. See the example on MDN.
The browser ignores everything after the first matching condition, so be careful how you order the media conditions.
Using either technique can save a lot of bandwidth. Older browsers that don't support these features will just ignore them and load the image referenced in the src attribute.
You typically automate the process using software such as Sharp to create multiple image sizes and formats. The HTML is often automatically generated as well.
To experiment with these techniques I uploaded pesto.jpg to responsivebreakpoints.com, downloaded the zip file and place the unzipped folder in the img directory.
There are also specialized image only hosting services such as Cloudinary which perform image processing.
Demo: samples of Cloudinary image processing:
<img
src="https://res.cloudinary.com/deedee/image/upload/w_200,h_200,c_crop/v1623521871/samples/pesto.jpg"
alt="basil"
/>
<img
src="https://res.cloudinary.com/deedee/image/upload/w_200,e_grayscale/v1623521871/samples/pesto.jpg"
alt="basil"
/>The techniques above are used primarily on high traffic websites. For smaller sites just run your images through a processor such as imageOptim before deploying them on your site.
The two column view applies only to widescreen.
We will make the article and aside run side by side by applying flex to their parent container within a mobile first breakpoint:
@media (width > 640px) {
.content {
display: flex;
}
}Note: we cannot use a CSS variable as a breakpoint, i.e.:
@media (min-width: var(--breakpoint)) {
/* ... */
}A media query is not an element selector so it does not inherit.
We can use the flex property on the flex children to manipulate the columns:
@media (width > 640px) {
.content {
display: flex;
}
article {
flex: 1 0 60%;
}
}The flex property is used on flex children only. We are using a shortcut here which includes flex-grow, flex-shrink, and flex-basis. The default is 0 1 auto.
Here is the long form:
article {
flex-grow: 1;
flex-shrink: 0;
flex-basis: 60%;
}Use a background color and box-shadow to color the aside:
@media (width > 640px) {
/* ommited for brevity */
aside {
background: var(--light-green);
box-shadow: -4px 0px 4px #ddd;
}
}Add a variable for light green:
--light-green: #fafdeb;Add some padding to the two columns for both large and small screens:
article,
aside {
padding: 1rem;
}Format the footer;
footer {
background-color: var(--basil-green);
padding: 1rem;
border-radius: 0 0 4px 4px;
margin-bottom: 2rem;
}Add the green background to the branding div.
header {
height: 120px;
background: var(--basil-green);
border-radius: 8px 8px 0px 0px;
}Note: this is one of the rare occasions that we use the height property. We can use it here because the header does not contain dynamic content.
Add a custom font (top of the css file). Copy the @font-face CSS from font/stylesheet.css into the top of styles.css and correct the file paths:
@font-face {
font-family: "futura_stdlight";
src:
url("font/futurastd-light-webfont-webfont.woff2") format("woff2"),
url("font/futurastd-light-webfont-webfont.woff") format("woff");
font-weight: normal;
font-style: normal;
}Note - To convert fonts to web formats see Font Squirrel.
header h1 {
background: url(img/basil.png) no-repeat;
font-family: futura_stdlight, sans-serif;
font-weight: normal;
color: #fff;
font-size: 5rem;
}Note: font-weight: normal; is nice to have here because by default header tags are bold and we do not have a bold version of the futura font available.
Try: temporarily remove no-repeat from the background property and setting the height of the <h1> to 600px.
The background image is 272px by 170px. Recall the final design goal.
Since background images fill the background of their container we can begin by manipulating padding:
header h1 {
/* omitted */
padding-left: 260px;
padding-top: 90px;
}(We cannot see the text because it is white and we have added padding.)
Use transform to tweak the positioning:
header h1 {
/* omitted */
transform: translate(-100px, -80px);
}Note: transforms are a useful property, especially when it comes to creating animations.
In order to simplify your CSS and maintain order you can nest one CSS rule inside another:
header {
height: 120px;
background: var(--basil-green);
border-radius: 8px 8px 0px 0px;
h1 {
background: url(img/basil.png) no-repeat;
font-family: futura_stdlight, sans-serif;
font-weight: normal;
color: #fff;
font-size: 5rem;
padding-left: 260px;
padding-top: 90px;
transform: translate(-100px, -80px);
}
}Note the beta link in the header:
<header>
<h1>Basilica!</h1>
<a class="beta" href="#">Beta</a>
</header>Absolutely position the beta element:
header a.beta {
background: url("img/burst.svg") no-repeat;
color: #fff;
font-size: 1.5rem;
position: absolute;
top: -20px;
right: 10px;
width: 85px;
height: 85px;
line-height: 85px;
text-align: center;
text-transform: uppercase;
}We can use flex in order to align the text vertically and horizontally:
header a.beta {
background: url("img/burst.svg") no-repeat;
color: #fff;
font-size: 1.5rem;
position: absolute;
top: -20px;
right: 10px;
width: 85px;
height: 85px;
/* line-height: 85px; */
/* text-align: center; */
text-transform: uppercase;
display: flex;
justify-content: center;
align-items: center;
}Note:
- the use of
img/burst.svgfor the background image. Examine it in the editor. - the use of position absolute. We will give this element a positioning context by applying position absolute to its containing element:
header {
/* omitted */
position: relative;
}Add a hover, transform and animate:
header a.beta {
/* omitted */
transform: rotate(20deg);
transition: all 1s ease;
}Create a hover state for the burst:
header a.beta:hover {
transform: rotate(0deg) scale(1.2);
}Finally, nest the beta element inside the header CSS:
header {
height: 120px;
background: var(--basil-green);
border-radius: 8px 8px 0px 0px;
position: relative;
h1 {
background: url(img/basil.png) no-repeat;
font-family: futura_stdlight, sans-serif;
font-weight: normal;
color: #fff;
font-size: 5rem;
padding-left: 260px;
padding-top: 90px;
transform: translate(-100px, -80px);
}
a.beta {
background: url("img/burst.svg") no-repeat;
color: #fff;
font-size: 1.5rem;
position: absolute;
top: -20px;
right: 10px;
width: 85px;
height: 85px;
text-transform: uppercase;
display: flex;
justify-content: center;
align-items: center;
transform: rotate(20deg);
transition: all 1s ease;
&:hover {
transform: rotate(0deg) scale(1.2);
}
}
}Note the use of the ampersand (&) for the nested hover.
Examine the page for problems in a narrow browser.
We will attempt a mobile first design strategy. Edit the css to display for small screen first:
header {
/* omitted */
h1 {
background: url(img/basil.png) no-repeat;
font-family: futura_stdlight, sans-serif;
font-weight: normal;
color: #fff;
font-size: 5rem;
}And add features for the large screen within the media query:
header {
/* omitted */
h1 {
background: url(img/basil.png) no-repeat;
font-family: futura_stdlight, sans-serif;
font-weight: normal;
color: #fff;
font-size: 5rem;
@media (width > 640px) {
padding-left: 240px;
padding-top: 90px;
transform: translate(-100px, -80px);
background-position: top left;
}
}
}Additional tweaks for the small screen:
- Remove the body margin top:
body {
font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
"Lucida Sans Unicode", Geneva, Verdana, sans-serif;
line-height: 1.5;
color: var(--dark-gray);
max-width: var(--max-width);
/* margin: 0 auto;
margin-top: 24px; */
}and add it back for the wide screen:
@media (min-width: 640px) {
margin: 0 auto;
margin-top: 24px;
}- Remove the rounded corners on small screen:
header {
/* omitted */
/* border-radius: 8px 8px 0px 0px; */
}add them back to wide screens:
header {
height: 120px;
background: var(--basil-green);
/* border-radius: 8px 8px 0px 0px; */
position: relative;
@media (width > 640px) {
border-radius: 8px 8px 0px 0px;
}
/* omitted */Always remember: there is no hover in touch screen devices. Use the Device Toggle in the developer tools set to a mobile viewport and tap on the burst.
Add the code below:
nav {
background: var(--light-gray);
border-top: 0.5rem solid var(--light-orange);
padding: 0.5rem;
display: flex;
align-items: center;
ul {
display: flex;
gap: 1rem;
}
li {
list-style: none;
/* margin-right: 0.5rem; */
}
p {
margin-right: auto;
}
}Note:
- the light gray needs to be lighter:
--light-gray: #e4e1d1; - we have two flex children, the
<ul>and the lone<p>tag. - the margin-right property on the paragraph and the effect it has on the positioning on the navigation links.
Remove it and add justify-content to the flex parent:
nav {
justify-content: space-between;
flex-wrap: wrap;
/* p {
margin-right: auto;
} */
}Note: the flex-wrap property allows the paragraph to stack on small screens.
nav {
/* omitted */
a {
text-align: center;
font-size: 1.5rem;
padding: 8px;
color: #fff;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
border-radius: 6px;
}
}The gradients for the buttons:
.nav-storeit a {
background-image: linear-gradient(to bottom, #fcde41 1%, #dfa910 100%);
}
.nav-storeit a:hover {
background-image: linear-gradient(to bottom, #dfa910 0%, #fcde41 100%);
}
.nav-pickit a {
background-image: linear-gradient(to bottom, #abc841 0%, #6b861e 100%);
}
.nav-pickit a:hover {
background-image: linear-gradient(to bottom, #6b861e 1%, #abc841 100%);
}
.nav-cookit a {
background-image: linear-gradient(to bottom, #6f89c7 0%, #344e8b 100%);
}
.nav-cookit a:hover {
background-image: linear-gradient(to bottom, #344e8b 1%, #6f89c7 100%);
}The red color we've chosen for hovers is not visually pleasant here. We will use the CSS pseudo-class :not to exclude the links in the nav bar:
nav {
/* omitted */
a {
/* omitted */
&:hover {
color: white;
}
}
}Make all the buttons the same width. Try with and without the inline-block.
nav {
/* omitted */
a {
/* omitted */
min-width: 120px;
display: inline-block;
}
}Note: this is a setting which will likely need to be changed to accomodate small screens.
CSS Tricks offers a guide to CSS grid.
Flexbox operates in a single dimension using the flex-direction property: x or y. CSS Grid operates on both the x and y axis.
Our current use of Flexbox to style the content columns operates in a single (horizontal or x) dimension so flex is really all we need for this layout.
Nevertheless, we will use CSS Grid for the layout in order to introduce some of its features in a simple use case.
Remove the flex statements and use a grid display, define columns, and set the start and end points for the grid children:
@media (width > 640px) {
.content {
/* display: flex; */
display: grid;
grid-template-columns: 20% 20% 20% 20% 20%;
}
article {
grid-column-start: 1;
grid-column-end: span 3;
/* flex: 1 0 60%; */
}
aside {
grid-column-start: 4;
grid-column-end: span 2;
background: var(--light-green);
box-shadow: -4px 0px 4px #ddd;
}
}grid-template-columns can also be expressed as a series of fractions:
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
Or using shorthand:
grid-template-columns: repeat(5, 1fr);
By moving the display: grid setting to the body selector, we can use grid areas to define our layout:
@media (width > 600px) {
body {
display: grid;
grid-template-areas:
"header"
"nav"
"content"
"footer";
}
header {
grid-area: header;
}
nav {
grid-area: nav;
}
.content {
grid-area: content;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-column-gap: 1rem;
}
article {
grid-column: span 3;
}
aside {
grid-column: span 2;
background: var(--light-green);
box-shadow: -4px 0px 4px #ddd;
}
footer {
grid-area: footer;
}
}Demo: with grid areas it becomes easy to reassign different sections of the page.
header {
grid-area: footer;
}This can be very useful in conjunction with breakpoints for responsively laying out complex pages.
Let's ease back into JavaScript with a demonstration and a simple DOM manipulation.
Review Node:
$ mkdir node
$ cd node
$ touch basilnode.js
$ npm init -y
$ npm install random-numberRandom Number on npmjs.com.
In basilnode.js:
const randomNumber = require("random-number");
const randomIndex = randomNumber({
min: 0,
max: 4,
integer: true,
});
console.log(randomIndex);
console.log(typeof randomNumber);
console.log(typeof randomIndex);At the command line:
$ node basilnode.jsAdd some additional variables - arrays:
const randomNumber = require("random-number");
const basilChef = ["mama", "papa", "baby"];
function randomItem(array) {
const randomIndex = randomNumber({
min: 0,
max: array.length - 1,
integer: true,
});
return array[randomIndex];
}
console.log(basilChef);
console.log(basilChef[0]);
console.log(basilChef.length);
console.log(randomItem(basilChef));$ node basilnode.jsCall the randomItem function from within another function and use a template string to construct a bit of HTML::
const randomNumber = require("random-number");
const basilChef = ["mama", "papa", "baby"];
const basilTexture = ["greasy", "frozen", "spicy"];
function randomItem(array) {
const randomIndex = randomNumber({
min: 0,
max: array.length - 1,
integer: true,
});
return array[randomIndex];
}
function makeBasil() {
return `<h2>${randomItem(basilChef)}'s ${randomItem(
basilTexture
)} basil</h2>`;
}
console.log(makeBasil());Here is the same random number generation above without relying on the installed node module:
function random() {
const max = 3;
const randomIndex = Math.floor(Math.random() * max);
return randomIndex;
}
const basilChef = ["mama", "papa", "baby"];
const basilTexture = ["greasy", "frozen", "spicy"];
function randomItem(array) {
const randomIndex = random();
return array[randomIndex];
}
function makeBasil() {
return `<h2>${randomItem(basilChef)}'s ${randomItem(
basilTexture
)} basil</h2>`;
}
console.log(makeBasil());<script src="js/index.js"></script>We'll do something similar to the node demo above in our app - replacing the recipe title with a random one.
In index.js:
Evolve a function that uses JavaScript's built-in Math methods to return a random number between zero and two:
function random() {
const max = 3;
// const randomIndex = Math.random();
// const randomIndex = Math.random() * max;
const randomIndex = Math.floor(Math.random() * max);
return randomIndex;
}
console.log(random());Now call our random function passing in an array.
const basilChefs = ["mama", "papa", "baby"];
function random(array) {
const max = array.length;
const randomIndex = Math.floor(Math.random() * max);
return array[randomIndex];
}
const recipeName = random(basilChefs);
console.log(recipeName);We used the random number to select a name from the array and return it to the calling function.
Add another variable basilTexture and massage the output to product a string:
const basilChefs = ["mama", "papa", "baby"];
const basilTexture = ["greasy", "frozen", "spicy"];
function random(array) {
const max = array.length;
const randomIndex = Math.floor(Math.random() * max);
return array[randomIndex];
}
const recipeName =
"My " + random(basilChefs) + "'s " + random(basilTexture) + " pesto";
console.log(recipeName);Let's use the return value in our layout:
const titleElement = document.querySelector("h2");Test titleElement in the console.
const titleElement = document.querySelector("h2");
const basilChefs = ["mama", "papa", "baby"];
const basilTexture = ["greasy", "frozen", "spicy"];
function random(array) {
const max = array.length;
const randomIndex = Math.floor(Math.random() * max);
return array[randomIndex];
}
const recipeName = `${random(basilChefs)}'s ${random(basilTexture)} pesto`;
titleElement.innerText = recipeName;and add CSS:
article h2 {
font-size: 2rem;
text-transform: capitalize;
}Create a div on the bottom of the html page (but before the script tag).
<div class="modal">
<h3>Hi! I'm a Modal Window (ʘ‿ʘ)╯</h3>
<p>Information about the beta program.</p>
</div>Add styling:
.modal {
box-sizing: border-box;
max-width: 400px;
padding: 2rem;
border-radius: 5px;
min-height: 200px;
border: 2px solid var(--orange);
background: white;
/* position: fixed;
top: calc(50vh - 100px);
left: calc(50vw - 200px); */
/* display: none; */
place-self: center;
}Uncomment display: none and add a open rule:
.open {
display: block;
}Test by adding the open class to the modal using the dev tool's inspector.
Code the .beta button to show the window.
Create a variable for the beta button, attach an event listener to it, and create a function to handle the event.
const modal = document.querySelector(".modal");
const betaButton = document.querySelector(".beta");
function showPopover(event) {
modal.classList.toggle("open");
event.preventDefault();
}
betaButton.addEventListener("click", showPopover);Refactor to use event delegation:
const modal = document.querySelector(".modal");
// const betaButton = document.querySelector('.beta');
function showPopover(event) {
console.log(event.target);
if (!event.target.matches(".beta")) return;
modal.classList.toggle("open");
event.preventDefault();
}
// betaButton.addEventListener('click', showPopover);
document.addEventListener("click", showPopover);- Use querySelector to find the first matching element on a page
const modal = document.querySelector('.modal'); - Use querySelectorAll() to find all matching elements on a page
- Use addEventListener('event', function), to listen for events on an element. You can find a full list of available events on the Mozilla Developer Network
- Use Functions to store and execute your commands
- Use classList to add, remove, toggle, list and test for classes
The matches() method lets you check if an element would be selected by a particular selector. It returns true if the element is a match, and false when it’s not. It can be an alternative to using element.classList.contains('.someclass').
// Match by an ID
if (elem.matches("#first-button")) {
// Do something...
}
// Match by a class
if (elem.matches(".button-submit")) {
// Do something...
}
// Match by one of several selectors
// Returns true when element contains at least one of the selectors
if (elem.matches(".click-me, .button-submit")) {
// Do something...
}Add html to the betainfo:
<div class="modal">
<h3>Hi! I'm a Modal Window (ʘ‿ʘ)╯</h3>
<p>Information about the beta program.</p>
<!-- NEW -->
<a class="closer" href="#0">✖︎</a>
</div>Style it:
.closer {
position: absolute;
top: -10px;
right: -10px;
width: 1.5rem;
height: 1.5rem;
background: #fff;
color: var(--orange);
border: 4px solid var(--orange);
border-radius: 50%;
text-align: center;
line-height: 1.5rem;
cursor: pointer;
}Extend the showPopover function to include the new element script.
const modal = document.querySelector(".modal");
function showPopover(event) {
if (!event.target.matches(".beta, .closer")) return;
modal.classList.toggle("open");
event.preventDefault();
}
document.addEventListener("click", showPopover);Note: you cannot animate between display: none and display: block.
Add a wrapping div - modal-outer - around the modal:
<div class="modal-outer">
<div class="modal">
<h3>Hi! I'm a Modal Window (ʘ‿ʘ)╯</h3>
<p>Information about the beta program.</p>
<a class="closer" href="#0">✖︎</a>
</div>
</div>Style it:
.modal-outer {
display: grid;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.5);
position: fixed;
height: 100vh;
width: 100vw;
top: 0;
left: 0;
/* Hide this until we need it */
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.modal-outer.open {
opacity: 1;
pointer-events: all;
}Try: changing the opacity and pointer-events properties to 1 and all.
Edit the script to select the outer div and apply the open class to it:
const modal = document.querySelector(".modal");
const modalOuter = document.querySelector(".modal-outer");
function showPopover(event) {
if (!event.target.matches(".beta, .closer")) return;
modalOuter.classList.toggle("open");
event.preventDefault();
}
document.addEventListener("click", showPopover);Now the modal wrapper will show when the button is clicked - but the modal will not. (Toggle the opacity on the outer div to see the modal.)
Edit styles for the interior modal:
.modal {
box-sizing: border-box;
max-width: 400px;
padding: 2rem;
border-radius: 5px;
min-height: 200px;
border: 2px solid var(--orange);
background: white;
transform: translateY(200%);
transition: transform 1s;
}Note: we are removing the following properties from the inner modal:
position: fixed;
top: 30%;
left: calc(50% - 150px);
display: none;Remove the generic open CSS rule:
/* .open {
display: block;
} */and replace it with a transform that selects the modal via the outer modal's class:
.modal-outer.open .modal {
transform: translateY(0);
}Note that we are no longer using 'display: none to hide the modal. The inner modal is becoming visible because its container, modal outer, is transitioning opacity.
Edit the script to allow clicking on the overlay to close the modal.
// const modal = document.querySelector('.modal')
const modalOuter = document.querySelector(".modal-outer");
function showPopover(event) {
if (event.target.matches(".beta")) {
modalOuter.classList.add("open");
} else if (event.target.matches(".closer, .modal-outer")) {
modalOuter.classList.remove("open");
} else return;
event.preventDefault();
}
document.addEventListener("click", showPopover);We will use the popover for different purposes depending on which element is clicked.
const modalOuter = document.querySelector(".modal-outer");
const modalInner = document.querySelector(".modal");
var betaContent = `
<h3>Oooops!</h3>
<p>Wow! Nothing works!<p>
`;
function showPopover(event) {
if (event.target.matches(".beta")) {
modalInner.innerHTML = betaContent;
modalOuter.classList.add("open");
} else if (event.target.matches(".closer, .modal-outer")) {
modalOuter.classList.remove("open");
} else return;
event.preventDefault();
}
document.addEventListener("click", showPopover);Let's use our new popover to display a different message when the user clicks on any of the three nav buttons.
const modalOuter = document.querySelector(".modal-outer");
const modalInner = document.querySelector(".modal");
var betaContent = `
<h3>Oooops!</h3>
<p>Wow! Nothing works!<p>
`;
var buttonContent = `
<h2>Coming Soon</h2>
<p>This feature coming soon.<p>
<a class="closer" href="#0">✖︎</a>
`;
function showPopover(event) {
if (event.target.matches(".beta")) {
modalInner.innerHTML = betaContent;
modalOuter.classList.add("open");
} else if (event.target.closest("nav ul")) {
modalInner.innerHTML = buttonContent;
modalOuter.classList.add("open");
} else if (event.target.matches(".closer, .modal-outer")) {
modalOuter.classList.remove("open");
} else return;
event.preventDefault();
}
document.addEventListener("click", showPopover);Note the use of closest above. The closest() method looks for the closest matching parent to an element that has a selector that you pass in.
<dialog class="modal">
<h3>Hi! I'm a Modal Window (ʘ‿ʘ)╯</h3>
<p>Information about the beta program.</p>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>// Get the button and modal let btn =
document.querySelector("[data-show-modal]");
let modal = document.querySelector("dialog"); // Show the modal
btn.addEventListener("click", function () {
modal.showModal();
});
modal.addEventListener("close", function (event) {
if (modal.returnValue !== "confirm") return;
console.log("Confirmed!");
// Do something...
});dialog::backdrop {
background-color: rgba(0, 0, 0, 0.7);
}