├── cardStyles.css ├── index.html ├── intro.css └── script.js /cardStyles.css: -------------------------------------------------------------------------------- 1 | *, *::before, *::after { 2 | font-family: 'Open Sans', sans-serif; 3 | } 4 | 5 | .card-grid { 6 | display: grid; 7 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 8 | gap: 1rem; 9 | align-items: flex-start; 10 | margin: 2rem; 11 | } 12 | 13 | .card { 14 | --padding: 1rem; 15 | background: white; 16 | border: 1px solid #777; 17 | border-radius: .25rem; 18 | overflow: hidden; 19 | } 20 | 21 | .card.card-shadow { 22 | border: none; 23 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .2); 24 | } 25 | 26 | .card-header { 27 | font-size: 1.5rem; 28 | padding: var(--padding); 29 | padding-bottom: 0; 30 | margin-bottom: .5rem; 31 | } 32 | 33 | .card-header.card-image { 34 | padding: 0; 35 | overflow: hidden; 36 | } 37 | 38 | .card-header.card-image > img { 39 | display: block; 40 | width: 100%; 41 | max-height: 200px; 42 | aspect-ratio: 16 / 9; 43 | object-fit: cover; 44 | object-position: center; 45 | transition: 200ms transform ease-in-out; 46 | } 47 | 48 | .card:hover > .card-header.card-image > img { 49 | transform: scale(1.025); 50 | } 51 | 52 | .card-body { 53 | font-size: .9rem; 54 | padding: 0 var(--padding); 55 | } 56 | 57 | .card-footer { 58 | margin-top: 1rem; 59 | padding: var(--padding); 60 | padding-top: 0; 61 | } 62 | 63 | .btn { 64 | --color: hsl(200, 50%, 50%); 65 | background: var(--color); 66 | color: white; 67 | border: none; 68 | font-size: 1rem; 69 | padding: .5em .75em; 70 | border-radius: .25em; 71 | cursor: pointer; 72 | } 73 | 74 | .btn:hover, .btn:focus { 75 | background: hsl(200, 50%, 60%); 76 | } 77 | 78 | .btn.btn-outline { 79 | background: none; 80 | border: 1px solid var(--color); 81 | color: var(--color); 82 | } 83 | 84 | .btn.btn-outline:hover, .btn.btn-outline:focus { 85 | background: hsl(200, 50%, 90%); 86 | } 87 | 88 | .btn + .btn { 89 | margin-left: .25rem; 90 | } 91 | 92 | .title { 93 | text-align: center; 94 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Document 14 | 15 | 16 |

Houses For Sale

17 |
18 |
19 |
20 | 21 |
22 |
23 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 24 |
25 | 29 |
30 |
31 |
32 | 33 |
34 |
35 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 36 |
37 | 41 |
42 |
43 |
44 | 45 |
46 |
47 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 48 |
49 | 53 |
54 |
55 |
56 | 57 |
58 |
59 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 60 |
61 | 65 |
66 |
67 |
68 | 69 |
70 |
71 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 72 |
73 | 77 |
78 |
79 |
80 | 81 |
82 |
83 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 84 |
85 | 89 |
90 |
91 |
92 | 93 |
94 |
95 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 96 |
97 | 101 |
102 |
103 |
104 | 105 |
106 |
107 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 108 |
109 | 113 |
114 |
115 |
116 | 117 |
118 |
119 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 120 |
121 | 125 |
126 |
127 |
128 | 129 |
130 |
131 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 132 |
133 | 137 |
138 |
139 |
140 | 141 |
142 |
143 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 144 |
145 | 149 |
150 |
151 |
152 | 153 |
154 |
155 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt expedita nulla nobis cumque quisquam. Enim perspiciatis vero laudantium nemo cum! 156 |
157 | 161 |
162 |
163 | 164 | -------------------------------------------------------------------------------- /intro.css: -------------------------------------------------------------------------------- 1 | .highlight-container { 2 | border: .1em solid black; 3 | border-radius: .25em; 4 | box-shadow: 0 0 0 9999999px rgba(0, 0, 0, .3); 5 | z-index: 9000; 6 | position: absolute; 7 | transition: 250ms ease-in-out; 8 | padding: .25rem; 9 | transform: translate(-.25rem, -.25rem); 10 | pointer-events: none; 11 | } 12 | 13 | .highlight-container.hide { 14 | border: none; 15 | } 16 | 17 | .modal { 18 | top: var(--y, 50%); 19 | left: var(--x, 50%); 20 | background-color: white; 21 | position: absolute; 22 | border: 1px solid #777; 23 | border-radius: .25em; 24 | max-width: 200px; 25 | z-index: 9001; 26 | opacity: 0; 27 | transform: scale(var(--scale)) translate(var(--translate)); 28 | transition: 150ms ease-in-out; 29 | } 30 | 31 | .modal.center { 32 | --translate: -50%, -50%; 33 | position: fixed; 34 | --y: 50% !important; 35 | --x: 50% !important; 36 | } 37 | 38 | .modal.show { 39 | --scale: 1; 40 | opacity: 1; 41 | } 42 | 43 | .modal .body, 44 | .modal .title { 45 | padding: 1rem; 46 | } 47 | 48 | .modal .title { 49 | font-size: 1.25em; 50 | font-weight: bold; 51 | padding-bottom: 0; 52 | } 53 | 54 | .modal .footer { 55 | border-top: 1px solid #777; 56 | padding: .5rem; 57 | display: flex; 58 | justify-content: space-between; 59 | } 60 | 61 | .modal .footer button { 62 | cursor: pointer; 63 | } 64 | 65 | .modal .close-btn { 66 | position: absolute; 67 | top: 0; 68 | right: 0; 69 | background: none; 70 | border: none; 71 | font-size: 1rem; 72 | cursor: pointer; 73 | color: #777; 74 | transition: 150ms ease-in-out; 75 | } 76 | 77 | .modal .close-btn:hover { 78 | color: black; 79 | } -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | class Modal { 2 | #modal 3 | #closeBtn 4 | #title 5 | #body 6 | #backBtn 7 | #nextBtn 8 | 9 | constructor(onBack, onNext, onClose) { 10 | this.#modal = document.createElement("div") 11 | this.#modal.classList.add("modal") 12 | 13 | this.#closeBtn = document.createElement("button") 14 | this.#closeBtn.innerHTML = "×" 15 | this.#closeBtn.classList.add("close-btn") 16 | this.#closeBtn.addEventListener("click", onClose) 17 | this.#modal.append(this.#closeBtn) 18 | 19 | this.#title = document.createElement("header") 20 | this.#title.classList.add("title") 21 | this.#modal.append(this.#title) 22 | 23 | this.#body = document.createElement("div") 24 | this.#body.classList.add("body") 25 | this.#modal.append(this.#body) 26 | 27 | const footer = document.createElement("footer") 28 | footer.classList.add("footer") 29 | this.#modal.append(footer) 30 | 31 | this.#backBtn = document.createElement("button") 32 | this.#backBtn.textContent = "Back" 33 | this.#backBtn.addEventListener("click", onBack) 34 | footer.append(this.#backBtn) 35 | 36 | this.#nextBtn = document.createElement("button") 37 | this.#nextBtn.textContent = "Next" 38 | this.#nextBtn.addEventListener("click", onNext) 39 | footer.append(this.#nextBtn) 40 | 41 | document.body.append(this.#modal) 42 | } 43 | 44 | set title(value) { 45 | this.#title.innerText = value 46 | } 47 | 48 | set body(value) { 49 | this.#body.innerText = value 50 | } 51 | 52 | show(value = true) { 53 | this.#modal.classList.toggle("show", value) 54 | } 55 | 56 | center(value = true) { 57 | this.#modal.classList.toggle("center", value) 58 | } 59 | 60 | position({ bottom, left }) { 61 | const offset = ".5rem" 62 | this.#modal.style.setProperty( 63 | "--x", 64 | `calc(${left + window.scrollX}px + ${offset})` 65 | ) 66 | this.#modal.style.setProperty( 67 | "--y", 68 | `calc(${bottom + window.scrollY}px + ${offset} + .25rem)` 69 | ) 70 | } 71 | 72 | remove() { 73 | this.#modal.remove() 74 | } 75 | 76 | enableBackButton(enabled) { 77 | this.#backBtn.disabled = !enabled 78 | } 79 | } 80 | 81 | class Intro { 82 | #modal 83 | #highlightContainer 84 | #bodyClick 85 | 86 | constructor(steps) { 87 | this.steps = steps 88 | this.#bodyClick = e => { 89 | if ( 90 | e.target === this.#currentStep.element || 91 | this.#currentStep.element?.contains(e.target) || 92 | e.target.closest(".highlight-container") != null || 93 | e.target.matches(".modal") || 94 | e.target.closest(".modal") != null 95 | ) { 96 | return 97 | } 98 | 99 | this.finish() 100 | } 101 | } 102 | 103 | start() { 104 | this.currentStepIndex = 0 105 | this.#modal = new Modal( 106 | () => { 107 | this.currentStepIndex-- 108 | this.#showCurrentStep() 109 | }, 110 | () => { 111 | this.currentStepIndex++ 112 | if (this.currentStepIndex >= this.steps.length) { 113 | this.finish() 114 | } else { 115 | this.#showCurrentStep() 116 | } 117 | }, 118 | () => this.finish() 119 | ) 120 | document.addEventListener("click", this.#bodyClick) 121 | this.#highlightContainer = this.#createHighlightContainer() 122 | this.#showCurrentStep() 123 | } 124 | 125 | finish() { 126 | document.removeEventListener("click", this.#bodyClick) 127 | this.#modal.remove() 128 | this.#highlightContainer.remove() 129 | } 130 | 131 | get #currentStep() { 132 | return this.steps[this.currentStepIndex] 133 | } 134 | 135 | #showCurrentStep() { 136 | this.#modal.show() 137 | this.#modal.enableBackButton(this.currentStepIndex !== 0) 138 | this.#modal.title = this.#currentStep.title 139 | this.#modal.body = this.#currentStep.body 140 | if (this.#currentStep.element == null) { 141 | this.#highlightContainer.classList.add("hide") 142 | this.#positionHighlightContainer({ x: 0, y: 0, width: 0, height: 0 }) 143 | this.#modal.center() 144 | } else { 145 | this.#modal.center(false) 146 | const rect = this.#currentStep.element.getBoundingClientRect() 147 | this.#modal.position(rect) 148 | this.#highlightContainer.classList.remove("hide") 149 | this.#positionHighlightContainer(rect) 150 | this.#currentStep.element.scrollIntoView({ 151 | behavior: "smooth", 152 | block: "center", 153 | inline: "center", 154 | }) 155 | } 156 | } 157 | 158 | #createHighlightContainer() { 159 | const highlightContainer = document.createElement("div") 160 | highlightContainer.classList.add("highlight-container") 161 | document.body.append(highlightContainer) 162 | return highlightContainer 163 | } 164 | 165 | #positionHighlightContainer(rect) { 166 | this.#highlightContainer.style.top = `${rect.top + window.scrollY}px` 167 | this.#highlightContainer.style.left = `${rect.left + window.scrollX}px` 168 | this.#highlightContainer.style.width = `${rect.width}px` 169 | this.#highlightContainer.style.height = `${rect.height}px` 170 | } 171 | } 172 | 173 | const intro = new Intro([ 174 | { 175 | title: "Test Title", 176 | body: "This is the body of the modal", 177 | }, 178 | { 179 | title: "Test Title 2", 180 | body: "This is the body of the modal 2", 181 | element: document.querySelector("[data-first]"), 182 | }, 183 | { 184 | title: "Test Title 3", 185 | body: "This is the body of the modal 3", 186 | element: document.querySelector("[data-second]"), 187 | }, 188 | ]) 189 | intro.start() 190 | 191 | setTimeout(() => { 192 | intro.finish() 193 | }, 2000) 194 | --------------------------------------------------------------------------------