├── 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 |
18 |
19 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------