MENU
- CSS Grid:
auto-fitvsauto-fill - Target
classPrefixes and Suffixes with|and$Selectors - Target external links with
a[href^="https"] - CSS Variables with JavaScript: Toggle background colour
- Perfectly-rounded buttons
- @media (hover: hover)
- Typographical Flow
- Centred, Variable Max-width Container
- Centre absolutely positioned ::after element
- Pixels to Rems
- Nested Grid Unusual Behaviour Fixed by
min-width:0 - CSS
text-box: trim-both cap alphabetic - The modern way to clear floats:
display: flow-root - CSS
box-shadowapplied to only one side - Invert image (darkmode / lightmode)
- Main Navigation Items Centred, Secondary Items Aligned Right
- Update Multiple Attributes
- Set Multiple Attributes (Deprecated)
- Global Event Listener
- If, if/else statement vs Conditional (ternary) operator
- Quick Fix for 'Uncaught TypeError: ITEM is undefined'
Math.ceil(Math.random() * n): Explanation- String Manipulation
- Multiple button instances that act independently of each other
- Check if element is in DOM with
.isConnectedthen.remove()element - Use
.some()array method to check if at least one element matches a condition loading="lazy"for all images, except the first- Workaround for Read-Only Imports in ES Modules: Getter/Setter Pattern
- Set
input type="date"to Current Date textareaCharacter Counter
- Copy Local Storage to JSON
- Clear local storage
- Delete Local Storage Keys
- Save Button Toggle Text to Local Storage
- No JS
- Accessible details/summary 'accordion'
- Accessible details/summary 'accordion' group
- Get Selected Option Value and Text
- Safari and List Semantics
- React Proptypes for Image Src
- React Proptypes and Default Proptypes for an Array of Objects
- React Button Component with props
- Temporarily Disable PropTypes
- React Router v6: 'end' replaces 'exact' in NavLink
- Vite/React: Dynamic Image Paths
- Pass Object as
Props - Simple Array: Use second
indexParameter of.map()Method to Supply Component'skeyValue - Get Random URL from an Array of Objects
- Set Array State
- Set Object State
- Setting State from Child Components
useEffect()Clean Up FunctionuseState()oruseState()anduseEffect()? Style-switcher Example- Get and Map Data with
async awaitand.map() - Loading Component with Animated Spinner
- Set Page Title with Component
- Git new branch: create, merge, delete, push
- Deleting Git
- Resolving Git Conflicts
- GitHub Markdown: Notes and Warnings
- GitHub Markdown: Add image to README.md
- GitHub Dependabot pull requests fail (because of outdated
deploy.yml)) - GitHub Pages: publish from
/docsafter bundling JS and minifying CSS - Post Dependabot PR Merge Local Workflow
- Delete old Windows Update files
- Weekly Check for ID 17 and IDs 18, 19 Events (Uncorrected Hardware Errors) in System Logs
Without, e.g. aria-labelledby="section-title"> and <h2 id="section-title">Section Title</h2> the section element has no semantic significance and might as well be a div.
(The aria-labelledby attribute identifies the element (or elements) that labels the element it is applied to.)
<section aria-labelledby="section-title">
<h2 id="section-title">Section Title</h2>
<p>Lorem ipsum...</p>
</section>Both auto-fit and auto-fill tell CSS Grid:
“Make as many columns as you can that are at least this wide.”
But the way they use extra space is different:
auto-fit tries to stretch your items to fill all the space, instead of creating empty columns.
- If the screen gets wider, the items themselves get wider.
- No empty "ghost" columns.
Think of it like arranging books on a shelf:
with auto-fit, the books spread out to fill the whole shelf.
.grid-auto-fit {
--min-column-width: 300px;
display: grid;
gap: 1rem;
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--min-column-width), 100%), 1fr)
);
}auto-fill creates as many columns as will fit, even if you don’t have enough items to fill them.
- Extra space becomes empty tracks.
- Items don’t stretch wider than your minimum width.
Using the book-shelf idea again:
with auto-fill, you keep adding invisible book-slots, even if you don’t have books for them.
.grid-auto-fill {
--min-column-width: 300px;
display: grid;
gap: 1rem;
grid-template-columns: repeat(
auto-fill,
minmax(min(var(--min-column-width), 100%), 1fr)
);
}The generic button styles will be applied to any class with prefix 'button'.
[class|="button"] {
/* Generic button styles */
background-color: blue;
color: white;
border-width: 1px solid transparent;
}
.button-add {
/* button add specific styles */
border-color: green;
}
.button-delete {
/* button delete specific styles */
border-color: red;
}The generic button styles will be applied to any class with suffix 'button'.
[class$="button"] {
/* Generic button styles */
background-color: red;
color: white;
border-width: 1px solid transparent;
}
.add-button {
/* button add specific styles */
border-color: purple;
}
.delete-button {
/* button delete specific styles */
border-color: white;
}Important
If you simply had a class of "button" on an element (<button class="button">Button</button>) then both selectors would try to apply the generic styles (and the last one defined would win). Better not to have such a monosyllabic class, but if you do, don't use the prefix and suffix selectors).
a[href^="https"] {
color: var(--accent-colour);
text-underline-offset: 0.5em;
&::after {
color: var(--accent-colour);
content: " \27F6";
}
}Manipulate a CSS variable with JavaScript.
<!DOCTYPE html>
<html lang="en" style>
<head>
...
</head>
<body>
<button id="change-body-bg" type="button">Toggle background colour of page</button>
</body>
</html>:root {
--body-bg: white;
--body-bg-alt: beige;
}
body {
background-color: var(--body-bg);
}const root = document.querySelector("html")
const bodyBgVal = "--body-bg"
const bodyBgAltVal = "var(--body-bg-alt)"
const btnChangeBodyBg = document.getElementById("change-body-bg")
btnChangeBodyBg.addEventListener("click", () => {
root.getAttribute("style") === ""
? root.style.setProperty(bodyBgVal, bodyBgAltVal)
: root.style.setProperty(bodyBgVal, null)
})<!-- On first click: -->
<html lang="en" style="--body-bg: var(--body-bg-alt);">
<!-- On toggle: -->
<html lang="en" style>
<!-- Etc. --><button type="button">Button</button>*,
*::after,
*::before {
box-sizing: border-box;
}
html {
font-size: 10px;
}
button {
all: unset;
background: blue;
color: white;
font-family: system-ui;
font-weight: 600;
font-size: 2rem;
padding: 1.6rem 2.4rem;
/**
Perfectly rounded left and right edges:
**/
border-radius: 100vw;
}Targets only those devices which support :hover and excludes those which don't, e.g., mobiles and tablets.
Useful if you find that a :hover state 'sticks' on mobile/tablet.
li a {
border-bottom: 5px solid blue;
}
/* Excludes mobiles and tablets from trying to :hover */
@media (hover: hover) {
li a:hover {
border-bottom-color: red;
}
}- Use for spacing mixed elements (h1,h2,h3, p, etc.) inside a container
The flow-em class will:
- add
margin-block-start: 1em(akamargin-top) to all elements after the first child of the container, - space the elements out proportionately, based on the font-size of the elements (which is why
emrather thanremis used).
* {
margin: 0;
}
.flow-em > * + * {
margin-block-start: 1em;
/* em NOT rem & margin-top NOT margin bottom */
}<article class="flow-em">
<h2>Main Heading</h2><!-- NO margin-top -->
<p>Some text.</p><!-- HAS margin-top -->
<p>Some text.</p><!-- HAS margin-top -->
<p>Some text.</p><!-- HAS margin-top -->
<!-- etc -->
</article>You can make flow-em more flexible by adding a custom variable:
.flow-em > * + * {
margin-block-start: var(--flow-space, 1em);
}Then you could change the margin-block-start value with an inline style:
<article class="flow-em" style="--flow-space: 1.5em;">
<h2>Main Heading</h2>
<p>Some text.</p>
<p>Some text.</p>
<p>Some text.</p>
<!-- etc -->
</article>Almost identical to flow-em, but this time using rem units.
Use for spacing child containers:
.flow-rem > * + * {
margin-block-start: 1rem;
}<article class="flow-rem">
<div>...</div>
<div>...</div>
<div>...</div>
<div>...</div>
<!-- etc -->
</article>Or:
.flow-rem > * + * {
margin-block-start: var(--flow-space, 1rem);
}<article class="flow-rem" style="--flow-space:1.5rem">
<div>...</div>
<div>...</div>
<div>...</div>
<div>...</div>
<!-- etc -->
</article>Ensures space on the left and right of the container once the max-width threshold has been crossed.
Note: No padding required on the container.
* {
box-sizing: border-box;
}
html {
font-size: 10px;
}
.container {
/* Locally-scoped CSS variables */
--_content-max-width: 120rem; /* i.e. 120 X 10px = 1200px */
--_content-space-outside: 2rem;
width: min(var(--_content-max-width), 100% - var(--_content-space-outside) * 2);
margin-inline: auto;
}<article class="container">
<h2>Main Heading</h2>
<p>Some text.</p>
<p>Some text.</p>
<p>Some text.</p>
</article>Centres both vertically and horizontally.
To only centre horizontally, use margin-inline: auto; in place of margin: auto;.
<div class="container"></div>*,
*::after,
*::before {
box-sizing: border-box;
}
html {
font-size: 10px;
}
.container {
position: relative;
width: 10rem;
aspect-ratio: 1;
/* Styling */
background: #000;
border-radius: 100vw;
padding: 1.6rem 2rem;
}
.container::after {
position: absolute;
width: max-content;
height: max-content;
inset: 0;
/*
Center horizontally:
margin-inline: auto;
*/
/* Center both vertically and horizontally: */
margin: auto;
/* Styling */
font-size: 4rem;
content: "\2705";
}Previously, I've addressed this by setting font-size: 10px in root:, then setting rems in the following way, e.g:
font-size: 1.6rem(= 16px)width: 72rem(= 720px)padding: 0.8rem 1.2rem(= 8px, 12px)
etc, etc.
This was to avoid having to calculate rems each time I wrote a CSS rule based on the browser's base font size of 16px.
However, I found this method had accessibility concerns: If a user sets his font size settings to, e.g. "Large", the page won't respond.
Assumption: You're using VSCode Editor.
- Install VSCode extension "Convert px to rem"
- During development, write all CSS using pixels.
When you're ready to publish:
Ctrl+Shift+PorCmd+Shift+P, type "convert px to rem", then hit theEnterkey.
Result: All pixel values will now be converted to rems.
Note
I recommend making a copy of the CSS file before you convert (and saving it as, e.g. "stylesPixels.css") so you have a reference if you want to make changes at a later date.
I inserted the following code into an HTML page:
<div class="items">
<div class="item">Item</div>
<div class="item">Item</div>
<div class="item">Item</div>
<div class="item">Item</div>
<div class="item">Item</div>
<!-- etc, etc up to 20 items -->
</div>I then applied the following CSS:
.items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
max-width: 1270px;
}
.item {
aspect-ratio: 3 / 2;
display: flex;
align-items: center;
justify-content: center;
background-color: blue;
}This worked as expected: On wide screens, the items were laid out in columns of 6. Making the window progressively smaller rendered columns of 5, 4, 3, 2 and 1.
To layout a page that ensures that the 'footer' always stays at the bottom I always use the following HTML/CSS:
<div class="page-layout">
<header>Header</header>
<main>Main content</main>
<footer>Footer</footer>
</div>.page-layout {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100dvh;
height: 100vh;
}<div class="page-layout">
<header>Header</header>
<main>
<div class="items">
<div class="item">Item</div>
<div class="item">Item</div>
<div class="item">Item</div>
<div class="item">Item</div>
<div class="item">Item</div>
<!-- etc, etc up to 20 items -->
</div>
</main>
<footer>Footer</footer>
</div>Result: The items grid remained stuck at 6 columns. Narrowing the browser window caused the items to first shrink, ultimately generating horizontal scrollbars when the space runs out.
This is because defining .page-layout as a grid container establishes a new grid formatting context. This affects how child elements inside it, including the .items grid, are sized and rendered.
main {
min-width: 0;
}- In a grid layout, items have a default
min-width: auto. This causes the grid item (in this case,main) to be at least as wide as its content, preventing it from shrinking as the browser window shrinks. - Setting
min-width: 0allows themainelement to shrink below its content width, thereby allowing the.itemsgrid inside it to adjust and reduce the number of columns as the window narrows.
Note
Both HTML pages had the usual <meta name="viewport" content="width=device-width, initial-scale=1.0"> tag in the head section.
This trims the 'virtual space' above and below text. it is currently (20/05/2025) not widely supported.
It can be used to trim text over a whole page, but this is overkill.
The best candidates for its application are:
- Lining up the top edge of text with an accompanying image.
- Evening-up the space above and below button text (although this can cause complications in non-supporting browsers, see below).
button {
all: unset; /* strips button of all default property values */
text-box: trim-both cap alphabetic;
padding-block: 0.5em;
/* Other styles */
padding-inline: 1em;
width: fit-content;
line-height: 1;
}The CSS above will successfully trim virtual space from above and below the button text. The padding-block: 0.5em will ensure that the text is evenly vertically spaced.
The button will be larger (taller), due to the vertical padding plus the virtual space. In order to counteract this, the following CSS might be used:
@supports not (text-box: trim-both cap alphabetic) {
button {
padding-block: 0.25em 0.4em; /* These values will depend on which font is used */
}
}- Lining up an image with text is the use-case with no fallbacks or magic numbers required: If the browser does not support the rule, you get what you have always got before.
- The problem with buttons has already been set out.
- Using it on all text blocks robs the page of the pleasing text rhythm supplied by the browser and requires fallbacks for non-supported browsers.
.container {
/* Self-clear floated children */
display: flow-root;
}
.floated-item {
float: left;
width: 100px;
height: 100px;
}<div class="container">
<div class="floated-item"></div>
<div class="floated-item"></div>
</div>Without display: flow-root; on .container, its height would collapse to zero.
| Direction | box-shadow Value |
|---|---|
| Bottom | 0 4px 6px rgba(0, 0, 0, 0.2) |
| Top | 0 -4px 6px rgba(0, 0, 0, 0.2) |
| Right | 4px 0 6px rgba(0, 0, 0, 0.2) |
| Left | -4px 0 6px rgba(0, 0, 0, 0.2) |
box-shadow: offset-x offset-y blur-radius spread-radius color;offset-x: horizontal offset (positive = right, negative = left)offset-y: vertical offset (positive = bottom, negative = top)blur-radius: how soft/diffuse the shadow isspread-radius: (optional) expands or contracts the shadowcolor: shadow color (usergba()for transparency)
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.2);box-shadow: 4px 0 6px rgba(0, 0, 0, 0.2);box-shadow: -4px 0 6px rgba(0, 0, 0, 0.2);The 6px in the examples is the blur radius, not the literal length of the shadow in one direction.
- A larger blur-radius (e.g.,
12px) creates a softer, more spread-out shadow - A blur-radius of 0 creates a sharp edge
box-shadow: 0 4px 0 rgba(0, 0, 0, 0.3); /* Sharp bottom shadow */box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); /* Soft, long shadow */Use spread-radius (4th value) to expand or shrink the shadow size:
box-shadow: 0 4px 6px 2px rgba(0, 0, 0, 0.2); /* 2px extra spread */<img
class="image-invert"
src="..."
alt=""
width=""
/>:root {
/* light mode (default) */
--image-invert: 0;
}
.darkmode {
--image-invert: 1;
}
.image-invert {
filter: invert(var(--image-invert));
}Previously, I'd tried (and failed) to achieve this using a single <ul>. Then the obvious solution presented itself: Put the secondary links into their own <ul> (which is still perfectly semantic):
<div class="navigation">
<nav>
<!-- Main links -->
<ul>
<li><a href="./">Home</a></li>
<li><a href="./blog.html">Blog</a></li>
<li><a href="./users.html">Users</a></li>
</ul>
<!-- Secondary links -->
<ul>
<li><a href="./about.html">About</a></li>
</ul>
</nav>
</div>.navigation {
padding-inline: 1rem;
& nav {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
/*
At e.g. < 400px (25rem) switch from grid to flex
so that uls line up next to each other.
*/
@media screen and (width <= 25rem) {
display: flex;
justify-content: center;
gap: 1.25rem;
}
}
& ul {
&:first-of-type {
grid-column: 2; /* Will be ignored at <= 400px because of display flex on parent */
display: flex;
gap: 1.25rem;
justify-content: center;
}
/* Will be ignored at <= 400px because of display flex on parent */
&:last-of-type {
grid-column: 3;
justify-self: end;
}
}
}- If an attribute value is non-null, it is added or updated.
- If an attribute value is null or undefined, it is removed.
Note
A hyphenated name must be written in quotes, e.g. "aria-label".
export default function updateAttributes(element, attributes) {
for (const [name, value] of Object.entries(attributes)) {
// Handles null and undefined
if (value == null) {
element.removeAttribute(name)
} else {
element.setAttribute(name, value)
}
}
}const someElement = document.querySelector(".some-element")
// Set attributes
updateAttributes(someElement, {
id: "1",
"aria-label": "introduction",
})
// Remove attributes
// A) With 'null' (recommended)
updateAttributes(someElement, {
id: null,
"aria-label": null,
})
// B) With 'undefined'
updateAttributes(someElement, {
id: undefined,
"aria-label": undefined,
})
// Mix setting and removing
updateAttributes(someElement, {
id: "2", // set or update
"aria-label": null, // remove
})See Update Multiple Attributes for a better version.
function setMultipleAttributes(element, attributesToSet) {
for (let i in attributesToSet) {
element.setAttribute(i, attributesToSet[i])
// i is the attribute(s)
// [i] is the attribute value(s)
}
}
// Example
const btnSubmit = document.createElement("button")
setMultipleAttributes(btnSubmit, {
type: "submit",
"data-submit-btn": "1",
"aria-pressed": "false"
// Note that attributes containing hyphens must be written as strings.
})
console.log(btnSubmit)<button type="submit" data-submit-btn="1" aria-pressed="false"></button>Migel Hewage Nimesha, DelftStack
Attach event listeners to dynamically generated elements.
If we have a set of hard-coded elements on an HTML page, it is simple to attach an event listener to all of them:
<div class="button-group">
<button class="button" type="button">Click</button>
<button class="button" type="button">Click</button>
<button class="button" type="button">Click</button>
</div>const buttons = document.querySelectorAll(".button")
buttons.forEach(button => {
button.addEventListener("click", () => {
console.log("clicked")
// Output: "clicked" per button click
})
})However, if you then dynamically create another button with the same class, the event listener will not be attached to it:
const buttonGroup = document.querySelector(".button-group")
const newButton = document.createElement("button")
newButton.classList.add("button")
newButton.textContent = "Click new"
buttonGroup.append(newButton)
// No output in console after clicking newButton<div class="button-group"></div>function globalEventListener(type, selector, callback, option = false) {
document.addEventListener(
type,
(e) => {
if (e.target.matches(selector)) callback(e)
},
option
)
}
const buttonGroup = document.querySelector(".button-group")
// Dynamically create 3 buttons inside 'button-group':
for (let i = 0; i < 3; i++) {
const newButton = document.createElement("button")
newButton.classList.add("button")
newButton.setAttribute("type", "button")
newButton.textContent = "Click new"
buttonGroup.append(newButton)
}
globalEventListener("click", ".button", (e) => {
console.log("New button clicked")
// Output: "New button clicked" per button click
})If we had a hard-coded input type="text" element, and we wanted to clear its value when the user clicked inside it, we would use the focus event on the click handler:
<form>
<input type="text" value="Enter some text" class="input-text" />
</form>const textInput = document.querySelector(".input-text")
textInput.addEventListener("focus", e => {
e.target.value = ""
})However, if we dynamically create a text input element and want the same behaviour, we cannot use the globalEventListener function with the focus event: Instead, we use focusin.
Furthermore, we have to override the default option = false parameter and add the argument true when we call the function.
<form></form>function globalEventListener(type, selector, callback, option = false) {
document.addEventListener(
type,
(e) => {
if (e.target.matches(selector)) callback(e)
},
option
)
}
const form = document.querySelector("form")
// Create the input element
const newTextInput = document.createElement("input")
newTextInput.setAttribute("type", "text")
newTextInput.value = "Enter some text"
newTextInput.classList.add("input-text")
globalEventListener(
// 'focusin' NOT 'focus'
"focusin",
".input-text",
(e) => {
e.target.value = ""
},
// add 'true' argument, overriding default 'option = false' parameter:
true
)
form.append(newTextInput)For a more detailed discussion see StackOverflow, JavaScript global event listener not working with focus event .
Ternary: composed of three.
<figure>
<img src="some-image.jpg" alt="">
<figcaption id="image-caption">Caption 1</figcaption>
</figure>
<button type="button" id="btn-caption">Change caption</button>const imageCaption = document.getElementById("image-caption")
const btnCaption = document.getElementById("btn-caption")
btnCaption.addEventListener("click", e => {
// EITHER ...
// If statement
if (imageCaption.textContent === "Caption 1") {
imageCaption.textContent = "Caption 2"
return
}
imageCaption.textContent = "Caption 1"
// OR ...
// If/else statement
if (imageCaption.textContent === "Caption 1") {
imageCaption.textContent = "Caption 2"
} else {
imageCaption.textContent = "Caption 1"
}
// OR ...
// Conditional (Ternary) operator V.1
imageCaption.textContent === "Caption 1"
? (imageCaption.textContent = "Caption 2")
: (imageCaption.textContent = "Caption 1")
// OR ...
// Conditional (Ternary) operator V.2
imageCaption.textContent =
imageCaption.textContent === "Caption 1" ? "Caption 2" : "Caption 1"
// ... will toggle the <figcaption> text.
})If the console prints an error message along the lines of ...
Uncaught TypeError: ITEM is undefined
... a potential quick fix is to wrap the offending ITEM in an if statement:
if (ITEM) {
// ITEM code ...
}The following code pushes ten numbers, in the range 1-6, into an array.
const arr = []
const numItems = 10
const n = 6
for (let i = 0; i < numItems; i++) {
arr.push(Math.ceil(Math.random() * n))
}
console.log(arr)Running this will result in, e.g., [1, 2, 6, 4, 1, 4, 1, 6, 1, 2].
Note
Each time you run the code, you'll get a different result (within the specified range).
Math.random()generates a random floating-point number between0(inclusive) and1(exclusive).
Note
'Inclusive': 0.0 can be generated. 'Exclusive': 1.0 cannot be generated, only a number approaching it, e.g., 0.999.
-
Math.ceil(Math.random() * n)(wheren = 6) scales up the random number by 6, resulting in a new floating-point number between0(inclusive) and6(exclusive). -
Example:
0.343 * 6 = 2.058. -
Math.ceil()will round this floating-point number up to the nearest integer, soMath.ceil(2.058) = 3. -
Therefore,
Math.ceil(Math.random() * 6)will generate an integer between 1 and 6 (inclusive).
Note
If 0.0 is generated by Math.random(), Math.ceil() will round this up to the nearest integer, i.e. 1.
const string = "Sample Sentence with a Few Words";
console.log(string);Output: "Sample Sentence with a Few Words"
const modifiedString = string.replace(/\s+/g, '-').toLowerCase();
console.log(modifiedString);Output: "sample-sentence-with-a-few-words"
const finalString = modifiedString.replace(/-/g, " ").replace(
/\w\S*/g,
(text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase()
);
console.log(finalString);Output: "Sample Sentence with a Few Words"
<section data-section>
<button data-button>Button 1</button>
<p data-output></p>
</section>
<section data-section>
<button data-button>Button 2</button>
<p data-output></p>
</section>
<section data-section>
<button data-button>Button 3</button>
<p data-output></p>
</section>const buttons = document.querySelectorAll("[data-button")
buttons.forEach(btn => {
btn.addEventListener("click", (e) => {
const section = btn.closest("[data-section]") /* Get parent container */
const output = section.querySelector("[data-output]") /* Target child element */
const buttonText = e.target.textContent
output.textContent = `${buttonText} output`
})
}).isConnected is a read-only boolean property of a DOM node that tells you whether the node is currently attached to the document (i.e. part of the live DOM).
<p id="content">Content</p>
<button id="remove-from-dom-btn" type="button">Remove static element from Dom</button>const removeFromDomBtn = document.getElementById("remove-from-dom-btn")
const content = document.getElementById("content")
removeFromDomBtn.addEventListener("click", () => {
if (content.isConnected) {
content.remove()
}
})<div id="created-element-container"></div>
<button id="remove-from-dom-btn" type="button">Remove dynamically-created element from Dom</button>const removeFromDomBtn = document.getElementById("remove-from-dom-btn")
const createdElementContainer = document.getElementById("created-element-container")
const createdElement = document.createElement("p")
createdElement.textContent = "Created element"
createdElementContainer.appendChild(createdElement)
removeFromDomBtn.addEventListener("click", () => {
if (createdElement.isConnected) {
createdElement.remove()
}
})const checkboxes = [
{ checked: false },
{ checked: false },
{ checked: true },
]
const anyChecked = checkboxes.some(box => box.checked)
console.log(anyChecked) // true, because one checkbox is checkedconst checkboxes = [
{ checked: false },
{ checked: true },
{ checked: true },
]
const anyChecked = checkboxes.some(box => box.checked)
console.log(anyChecked) // true, because at least one checkbox is checkedconst checkboxes = [
{ checked: false },
{ checked: false },
]
console.log(checkboxes.some(box => box.checked)) // false, because no checkboxes are checked
const images = document.querySelectorAll("img")
let imageCount = 0
function lazyLoadImages(image) {
if (imageCount > 0) {
image.loading = "lazy"
}
imageCount++;
}
images.forEach(image => {
lazyLoadImages(image)
})The following will throw a console error:
globals.js:
export let myVarindex.js:
import { myVar } from "./globals.js"
myVar = 10
console.log("myVar: Assignment to constant variable")This is because:
- Named imports are read-only bindings, even if the original was declared with
letorvar. - You can read their value, but cannot assign to them in the importing module.
To modify a shared value you can use setter/getter functions:
globals.js:
let myVar
export function setMyVar(val) {
myVar = val
}
export function getMyVar() {
return myVar
}index.js:
import { setMyVar, getMyVar } from "./globals.js"
setMyVar(10)
console.log(getMyVar()) // 10<input
type="date"
id="date"
/>const today = new Date().toISOString().split("T")[0]
document.getElementById("date").value = todayp:empty {
display: none;
}<form>
<label for="textbox">
Add a brief image description (max 150 characters)
</label>
<textarea
name="alt-text"
id="textbox"
maxlength="150"
></textarea>
<div id="char_count">0/150</div>
<p id="char_limit_message"></p>
</form>const textArea = document.getElementById("textbox")
const characterCounter = document.getElementById("char_count")
const maxNumOfChars = 150
const charLimitMessage = document.getElementById("char_limit_message")
function countCharacters() {
let numOfEnteredChars = textArea.value.length
let counter = maxNumOfChars - numOfEnteredChars // Output 149, 148, 147, etc.
characterCounter.textContent = `${maxNumOfChars - counter}/150` // Output 1/150, 2/150, 3/150, etc.
charLimitMessage.textContent =
numOfEnteredChars === 150 ? "No more characters allowed!" : ""
}
textArea.addEventListener("input", countCharacters)index.html, etc.
<html
lang="en"
class="no-js"
>
<head>
<script>
document.documentElement.classList.remove("no-js")
</script>
[Rest of 'head' items]
</head>
</html>no-js.css:
.no-js {
& .theme-toggler,
& .hamburger-button-wrapper,
& .loader,
& .loader::after {
display: none;
}
/* etc. */
}
style.css:
@import "./no-js.css";
/* All other @imports */Note
If the project is completely dependent on JavaScript, use the same code as above but also add a <noscript> message:
index.html, etc:
<body>
<noscript>
<div class="noscript-banner">
JavaScript is required for this site to function.
</div>
</noscript>
[Rest of content...]
</body>Then add .noscript-banner styles to no-js.css, e.g.
.noscript-banner {
position: fixed;
top: 30%;
left: 0;
right: 0;
background-color: var(--warning-bg);
color: var(--warning);
font-weight: var(--bold);
text-align: center;
padding: 0.75rem 1rem;
z-index: 9999;
border-block: 0.125rem solid var(--warning);
}Note
If you want to use CSS variables in no-js.css place @import "./no-js.css" immediately after @import "./root.css".
<details id="details">
<summary aria-controls="#details" id="summary" aria-expanded="false">
<span id="summary-status">Open</span> details
</summary>
<p>Details content...</p>
</details>const details = document.getElementById("details")
const summary = document.getElementById("summary")
const summaryStatus = document.getElementById("summary-status")
details.addEventListener("toggle", () => {
// Note: the browser adds and removes the 'open' attribute
if (details.open) {
summary.setAttribute("aria-expanded", "true")
summaryStatus.textContent = "Close"
} else {
summary.setAttribute("aria-expanded", "false")
summaryStatus.textContent = "Open"
}
}).summary {
cursor: pointer;
}- Initially, all summaries are closed.
- Once a summary is opened, clicking on another item will close the previous one.
- Click again on an opened summary and it will self-close.
summary { cursor: pointer; }
/**
Hides 'Open' and 'Close' from view.
Exposes the text content of label 'data-summary-label'
to screen readers only.
*/
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(0);
border: 0;
}<section data-details-group>
<h2>Details</h2>
<details id="details-1">
<summary aria-controls="#details-1" aria-expanded="false">
<span data-summary-label class="visually-hidden">Open </span>Summary title #1
</summary>
<p>Lorem ipsum dolor sit amet.</p>
</details>
<details id="details-2">
<summary aria-controls="#details-2" aria-expanded="false">
<span data-summary-label class="visually-hidden">Open </span>Summary title #2
</summary>
<p>Lorem ipsum dolor sit amet. Lorem, ipsum dolor.</p>
</details>
<details id="details-3">
<summary aria-controls="#details-3" aria-expanded="false">
<span data-summary-label class="visually-hidden">Open </span>Summary title #3
</summary>
<p>Lorem ipsum dolor sit amet. Lorem, ipsum dolor. Lorem ipsum dolor sit.</p>
</details>
</section>const detailsItems = document.querySelectorAll("[data-details-group] details")
const summaryItems = document.querySelectorAll("[data-details-group] summary")
closeOtherOpenedDetails(summaryItems)
accessibleDetails(detailsItems)
function closeOtherOpenedDetails(summaries) {
summaries.forEach((summary) => {
summary.addEventListener("click", (e) => {
summaries.forEach((summary) => {
const details = summary.closest("details")
const summaryClicked = e.target.closest("details")
if (details != summaryClicked) {
details.removeAttribute("open")
}
})
})
})
}
// Adds accessibility information for screen readers
function accessibleDetails(details) {
details.forEach(detail => {
detail.addEventListener("toggle", () => {
const summary = detail.querySelector("summary")
const summaryLabel = detail.querySelector("[data-summary-label]")
if (detail.open) {
summary.setAttribute("aria-expanded", "true")
summaryLabel.textContent = "Close "
} else {
summary.setAttribute("aria-expanded", "false")
summaryLabel.textContent = "Open "
}
})
})
}<form>
<select name="select-nums-list" id="select-nums-list">
<option value="none">Select a number</option>
<option value="0">Zero</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
<option value="4">Four</option>
<option value="5">Five</option>
</select>
</form>
<!-- Output: -->
<ul>
<li><b>Selected option value: </b><span id="selected-num-value"></span></li>
<li><b>Selected option text: </b><span id="selected-num-text"></span></li>
</ul>const selectNumsList = document.getElementById("select-nums-list")
const selectedNumValue = document.getElementById("selected-num-value")
const selectedNumText = document.getElementById("selected-num-text")
getSelectedOptionValueAndText(selectNumsList, selectedNumValue, selectedNumText)
function getSelectedOptionValueAndText(select, value, text) {
select.addEventListener("change", e => {
const optionValue = e.target.value
const optionText = e.target.options[e.target.selectedIndex].text
if (optionValue === "none") {
value.textContent = ""
text.textContent = ""
} else {
value.textContent = optionValue
text.textContent = optionText
}
})
}If you add list-style: noneto a ul, or list-style-type : none to an li and listen to the output in a screen reader, with the page loaded in the Safari browser, the semantic value is removed. This means that a list of items won't be identified as such; they will merely be a collection of items.
Here are a couple of fixes, the second one being the best, in my opinion:
ul { list-style: none;}
/* OR: */
li { list-style-type: none;}<ul role="list">
<li>Item</li>
<li>Item</li>
<li>Item</li>
</ul>Src: "Fixing" Lists
li { list-style-type: ""}Src: Here’s what I didn’t know about list-style-type
In Chrome:
- open the code inspector and go to the 'Applications' tab.
- Select and copy the 'Key' of the project.
- Paste the key into the following code snippet:
copy(localStorage.getItem("Key-goes-here"))- Switch to the console tab and paste the snippet in the window.
- The contents of local storage for that key has now been saved to the clipboard.
- Open a text editor.
Edit > Paste.- Save the file as
[name].json.
Clicking the button launches a confirm dialog. If you click 'yes', local storage will be cleared.
Useful for local development on the VSCode server. Not recommended as a production option because if the user is running the project from the file location, clicking the button will clear local storage for every project that is using this location.
<button id="clear-local-storage" type="button">
Clear local storage
</button>const clearLocalStorage = document.getElementById("clear-local-storage")
clearLocalStorage.addEventListener("click", () => {
if (window.confirm("Do you really want to clear all local storage?")) {
window.localStorage.clear()
}
})<button class="delete-all-entries" data-delete-all-entries>
Delete all entries
</button>const deleteAllBtn = document.querySelector("[data-delete-all-entries]")it is easy to delete all local storage, but that's not always what you want.
For instance, you could be running multiple apps from the local file system (file:///C:/Users/... on Windows) each app using differently named local storage keys.
If you deleted all local storage, all the apps would return to their default state.
function deleteEntries() {
deleteAllBtn.addEventListener("click", () => {
if (window.confirm("Do you really want to delete all entries?")) {
window.localStorage.clear()
window.location.reload()
}
})
}
deleteEntries()In this example, only the LOCAL_STORAGE_KEY-table-entries key will be deleted, leaving any other keys intact.
function deleteEntries() {
deleteAllBtn.addEventListener("click", () => {
if (window.confirm("Do you really want to delete this key?")) {
const keyToRemove = "LOCAL_STORAGE_KEY-table-entries"
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key.startsWith(keyToRemove)) {
localStorage.removeItem(key)
}
}
window.location.reload()
}
})
}
deleteEntries()In this example, both the LOCAL_STORAGE_KEY-table-entries and LOCAL_STORAGE_KEY-button-state keys will be deleted, leaving all other keys intact.
function deleteEntries() {
deleteAllBtn.addEventListener("click", () => {
if (window.confirm("Do you really want to delete these 2 keys?")) {
const keysToRemove = ["LOCAL_STORAGE_KEY-table-entries","LOCAL_STORAGE_KEY-button-state"]
keysToRemove.forEach((keyToRemove) => {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key.startsWith(keyToRemove)) {
localStorage.removeItem(key)
}
}
})
window.location.reload()
}
})
}
deleteEntries()<button id="btn">Button text A</button>const LOCAL_STORAGE_KEY = "button-toggle-text"
const btn = document.getElementById("btn")
// Save button toggle text to local storage on button click
btn.addEventListener("click", (e) => {
if (e.target.textContent === "Button text A") {
localStorageButtonText(e, "Button text B")
} else if (e.target.textContent === "Button text B") {
localStorageButtonText(e, "Button text A")
}
})
function localStorageButtonText(e, btnText) {
localStorage.setItem(LOCAL_STORAGE_KEY, btnText)
const storedBtnText = localStorage.getItem(LOCAL_STORAGE_KEY)
e.target.textContent = storedBtnText
}
/*
Set initial button text on page load,
if text has been saved, i.e. button has already been clicked.
*/
function setInitialButtonText() {
const storedBtnText = localStorage.getItem(LOCAL_STORAGE_KEY)
/*
'if' statement here ensures that function will only run
after text has already been stored.
*/
if (storedBtnText) {
btn.textContent = storedBtnText
}
}
setInitialButtonText()For each project using IndexedDB:
- Make sure the database name is unique, e.g.
// Journal project
const DB_NAME = "journal2025"
// To-Do list project
const DB_NAME = "toDoList"
// Photo gallery project
const DB_NAME = "photoGallery"This will ensure that multiple projects hosted on GitHub Pages have separate data stores.
A further step is required if your server is local. In the project root:
- Create a folder called
.vscode. Inside this - create a file called
settings.json. Inside this, add the following code, e.g.
{
"liveServer.settings.port": 5501
}- This will launch the project using port 5501.
- Use a new port number for each new project.
Note
The default port for LiveServer is 5500. The safe range for custom ports is 5501–5999.
Proptypes for both locally- and externally-sourced files.
import PropTypes from "prop-types"
function Image(props) {
return (
<img
src={props.image}
alt=""
/>
)
}
Image.propTypes = {
image: PropTypes.oneOfType([
PropTypes.string, // image sourced from 'assets/'
PropTypes.instanceOf(URL), // image sourced from external URL
]),
}
export default Imageimport Image from "./Image.jsx"
import AssetsImage from "../../assets/image.jpg"
function ImagesContainer() {
return (
<section>
<Image image={AssetsImage} />
<Image image="https://path-to-external-file/image.jpg" />
</section>
)
}
export default ImagesContainer<section>
<img src="/src/assets/image.jpg" alt="">
<img src="https://path-to-external-file/image.jpg" alt="">
</section>Note
Component.jsx: Default values for items are included in conditional statements, e.g.,
<b>Name</b>: {item.name ? item.name : "No name supplied"}Note
Component.jsx: The placeholder image is imported and also included in a conditional statement:
import placeholderProfilePic from "../../assets/staff/placeholder.jpg"
<img
src={item.profilePic ? item.profilePic : placeholderProfilePic}
alt={item.name ? item.name : "Placeholder profile image"}
/>import PropTypes from "prop-types"
import placeholderProfilePic from "../../assets/staff/placeholder.jpg"
function Component(props) {
const itemList = props.items
const staffListItemsAll = itemList.map((item) => (
<li key={item.id}>
<img
src={item.profilePic ? item.profilePic : placeholderProfilePic}
alt={item.name ? item.name : "Placeholder profile image"}
/>
<ul>
<li>
<b>Name</b>: {item.name ? item.name : "No name supplied"}
</li>
<li>
<b>Age</b>: {item.age ? item.age : "No age supplied"}
</li>
<li>
<b>Status</b>: {item.category ? item.category : "Status unknown"}
</li>
<li>
<b>Description</b>:{" "}
{item.description ? item.description : "No description supplied"}
</li>
</ul>
</li>
))
return (
<>
<h3>{props.title}</h3>
<ul>{staffListItemsAll}</ul>
<hr />
</>
)
}
Component.propTypes = {
title: PropTypes.string,
// PropTypes for array of objects:
items: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
age: PropTypes.number,
description: PropTypes.string,
category: PropTypes.string,
profilePic: PropTypes.oneOfType([
PropTypes.string, // image sourced from assets/
PropTypes.instanceOf(URL), // image sourced from external URL
]),
})
),
}
Component.defaultProps = {
title: "No title supplied",
items: [],
}
export default Componentimport Component from "./Component"
import StaffPic1 from "../../assets/staff/staffpic-1.jpg"
import StaffPic2 from "../../assets/staff/staffpic-2.jpg"
import StaffPic3 from "../../assets/staff/staffpic-3.jpg"
import StaffPic4 from "../../assets/staff/staffpic-4.jpg"
import StaffPic5 from "../../assets/staff/staffpic-5.jpg"
function ComponentParent() {
const staffAll = [
{
id: 1,
name: "John Self",
age: 40,
description: "CEO. Does not suffer fools gladly.",
category: "Management",
profilePic: StaffPic1,
},
{
id: 2,
name: "Jane Doe",
age: 25,
description: "Quiet quitter.",
category: "Staff",
profilePic: StaffPic2,
},
{
id: 3,
name: "Helmut Kopf",
age: 33,
description: "Systems analyst, currently under investigation by the Met.",
category: "Management",
profilePic: StaffPic3,
},
{
id: 4,
name: "Susan Queue",
age: 70,
description: "Former rock star, hobbies include gardening.",
category: "Staff",
profilePic: StaffPic4,
},
{
id: 5,
name: "Chris Walken",
age: 61,
description: "Accountant. No relation to the famous film star.",
category: "Staff",
profilePic: StaffPic5,
},
{
id: 6,
name: "Abigail Fiesta",
age: 23,
description: "HR consultant working from home in Hammersmith, London.",
category: "Staff",
// img path is from external source:
profilePic:
"https://clipground.com/images/woman-profile-picture-clipart-9.jpg",
},
// Only id supplied.
{
id: 7,
},
]
const managers = staffAll.filter((member) => member.category === "Management")
const notManagers = staffAll.filter((member) => member.category !== "Management")
return (
<section>
<h2>Staff List</h2>
{staffAll.length > 0 && (
<Component
items={staffAll}
title="All Staff"
/>
)}
{managers.length > 0 && (
<Component
items={managers}
title="Management"
/>
)}
{notManagers.length > 0 && (
<Component
items={notManagers}
title="Not management"
/>
)}
<Component />
</section>
)
}
export default ComponentParentimport PropTypes from "prop-types"
function Button(props) {
return (
<button
style={props.style}
onClick={props.onClick}
>
{props.children}
</button>
)
}
Button.propTypes = {
style: PropTypes.object,
children: PropTypes.string,
onClick: PropTypes.func.isRequired,
}
export default Buttonimport Button from "./Button.jsx"
function App() {
const handleClick = () => {
console.log("Button was clicked!")
}
// Function with parameter
const handleClick2 = (e) => {
console.log(e.target.textContent)
}
return (
<Button
onClick={handleClick}
style={{
background: "red",
color: "white",
cursor: "pointer",
}}
>
Click Me
</Button>
// Button with parameter
<Button
onClick={(e) => handleClick2(e)}
style={{
background: "blue",
color: "white",
cursor: "pointer",
}}
>
Click Me (e)
</Button>
)
}
export default AppWhen using props in a 'jsx' file, VSCode prompts for proptypes definitions by default. If you don't add them immediately, the file is marked in red. This can be annoying. To put off defining proptypes until later, add the following code at the very top of the file:
/* eslint-disable react/prop-types */Removing it will trigger the proptypes prompt once again.
Say you have the following 2 routes:
const router = createBrowserRouter([
// other code ...
{
path: "/post",
element: <CreatePost />,
},
{
path: "/post/:id",
element: <Post />,
},
]}and in the main navigation, you have the following NavLink:
<NavLink
to="/post"
className={({ isActive }) => {
return isActive ? "nav-active" : ""
}}
>
Create Post
</NavLink>- The path '/post' takes you to a form, where can you create a new post.
- The path '/post/:id' takes you to a created post, with a url like `/post/1'.
You don't want the 'Create Post' NavLink to be highlighted when you go to an actual post, so you can add end to the NavLink:
<NavLink
to="/post"
className={({ isActive }) => {
return isActive ? "nav-active" : ""
}}
end // This will limit the url to '/post' only !
>
Create Post
</NavLink>Note
In React Router < v6, exact was used in place of end. However, I'm not familiar with the actual details of how you would use exact in v5.
For dynamic image paths, store the images in the /public/ folder. You can put them in a sub-folder, in this case animals/.
const animals = [
{
// Other key/value pairs
image: "cat.png",
// Other key/value pairs
},
// More objects...
]<Animal
// Other props
src={animals.image}
// Other props
/><img src={`/site-name/animals/${src}`} />All dynamic images are stored in /public/animals.
Warning
You must NOT include '/public/' in the file path, or the images won't display.
To pass props as an object from Parent.jsx to the Child.jsx component, the names in Child.jsx must be identical to the keys in the data object.
export default [
{
id: 1,
firstName: "John",
lastName: "Smith",
address: {
street: "High Street",
houseNumber: 44,
postCode: "SE33 4LG",
},
taxId: "1234ABCD",
},
// More objects
]import Child from "./Child"
import data from "./data"
function Parent() {
const items = data.map((item) => {
return (
<Child
key={item.id}
item={item} // Pass props as object to Child.jsx
/>
)
})
return <ul>{items}</ul>
}
export default Parentimport PropTypes from "prop-types"
/*
Access key values in data.js using object dot notation,
prefixed by prop object {item}, e.g.,
item.firstName, etc.
Get a nested key value, e.g.,
item.address.street
*/
function Child({ item }) {
return (
<li>
<h2>{`${item.firstName} ${item.lastName}`}</h2>
<h3>Address</h3>
<p>
<span>{`${item.address.houseNumber} ${item.address.street},`}</span>
<span>{item.address.postCode}</span>
</p>
<h3>Tax ID</h3>
<p>{item.taxId}</p>
</li>
)
}
Child.propTypes = {
item: PropTypes.shape({
firstName: PropTypes.string,
lastName: PropTypes.string,
address: PropTypes.shape({
street: PropTypes.string,
houseNumber: PropTypes.number,
postCode: PropTypes.string,
}),
taxId: PropTypes.string,
}),
}
export default ChildIn the absence of an id (which would probably be present in an array of objects) use .map(item, index).
function App() {
const itemsArray = ["Item 1", "Item 2"]
const items = itemsArray.map((item, index) => {
return <p key={item[index]}>{item}</p>
})
return <>{items}</>
}
export default Appexport default {
data: {
images: [
{
id: "1",
title: "Image 1",
url: "https://randomImage.com/random-image-1.jpg",
},
{
id: "2",
title: "Image 2",
url: "https://randomImage.com/random-image-2.jpg",
},
// many more objects ...
],
},
}import imageData from "../imageData.js"
function LogRandomUrls() {
function getImageUrls() {
const imageArray = imageData.data.images
const randomNumber = Math.floor(Math.random() * imageArray.length)
const url = imageArray[randomNumber].url
console.log(url)
}
return <button onClick={getImageUrls}>Log random URL</button>
}
export default LogRandomUrlsimport { useState } from "react"
function Items() {
const [itemsArray, setItemsArray] = useState(["Item 1", "Item 2"])
function addItem() {
setItemsArray((prevItemsArray) => {
return [...prevItemsArray, `Item ${prevItemsArray.length + 1}`]
})
}
const listItems = itemsArray.map((item) => <li key={item}>{item}</li>)
return (
<div>
<button onClick={addItem}>Add Item</button>
<ul>{listItems}</ul>
</div>
)
}
export default Itemsimport { useState } from "react"
function Contact() {
const [contact, setContact] = useState({
firstName: "John",
lastName: "Doe",
phone: "+44 (207) 391 4023",
email: "name@example.com",
isFavorite: false, // To be changed
})
let starIcon = contact.isFavorite ? "star-filled.png" : "star-empty.png"
function toggleFavorite() {
setContact((prevContact) => ({
...prevContact,
isFavorite: !prevContact.isFavorite, // Set new value
}))
}
return (
<div>
{/* Toggle new value*/}
<img
src={`../images/${starIcon}`}
onClick={toggleFavorite}
/>
<ul>
<li>{`Name: ${contact.firstName} ${contact.lastName}`}</li>
<li>{`Tel: ${contact.phone}`}</li>
<li>{`Email: ${contact.email}`}</li>
</ul>
</div>
)
}
export default Contactimport { useState } from "react"
import Star from "./Star"
export default function Favourite() {
const [status, setStatus] = useState({
isFavorite: false,
})
function toggleFavorite() {
setStatus((prevStatus) => ({
...prevStatus,
isFavorite: !prevStatus.isFavorite,
}))
}
return (
<Star
isFilled={status.isFavorite}
handleClick={toggleFavorite}
/>
)
}import PropTypes from "prop-types"
function Star({ isFilled, handleClick }) {
const starIcon = isFilled ? "star-filled" : "star-empty"
const buttonLabel = isFilled ? "Unmark as favourite" : "Mark as favourite"
return (
<button
onClick={handleClick}
aria-label={buttonLabel}
aria-pressed={isFilled}
>
<img
src={`/${starIcon}.svg`}
alt="Star icon"
/>
</button>
)
}
Star.propTypes = {
isFilled: PropTypes.bool,
handleClick: PropTypes.func.isRequired,
}- When the button state is set to
true, thewindow.innerWidthis displayed in theh1. - When it is toggled to
false, theh1is hidden. - However, if the clean up function is not included (in this case, removing the event listener)
window.innerWidthwill continue to run in the background, even if its output is not displayed.
import PropTypes from "prop-types"
import { useState, useEffect } from "react"
export default function App() {
const [show, setShow] = useState(true)
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
function toggle() {
setShow((prevShow) => !prevShow)
}
return (
<div className="container">
<button onClick={toggle}>Toggle WindowTracker</button>
{show && (
<WindowTracker
windowState={windowWidth}
setWindowState={setWindowWidth}
/>
)}
</div>
)
}
function WindowTracker({ windowState, setWindowState }) {
useEffect(() => {
function watchWidth() {
setWindowState(window.innerWidth)
}
window.addEventListener("resize", watchWidth)
// Clean up function
return function () {
window.removeEventListener("resize", watchWidth)
}
}, [setWindowState])
return <h1>Window width: {windowState}</h1>
}
WindowTracker.propTypes = {
windowState: PropTypes.number,
setWindowState: PropTypes.func,
}- If you want to e.g., simply toggle the style of a web page, use
useState(). - If you want to e.g., toggle the style of a web page AND save the selected style to
localstorage, useuseEffect()as well asuseState().
import { useState } from "react"
import BtnStyleSwitcher from "./components/BtnStyleSwitcher"
function App() {
const [mode, setMode] = useState(true)
function handleMode() {
setMode((prevMode) => !prevMode)
}
return (
<>
<BtnStyleSwitcher
handleClick={handleMode}
mode={mode}
/>
<div className={`content ${mode ? "darkmode" : ""}`}>
<p>
Page content ... ipsum dolor sit amet consectetur adipisicing elit.
Eaque impedit repudiandae necessitatibus sequi accusamus unde sed
animi similique, quia maxime alias nihil nesciunt? Incidunt dolorem
cum deserunt, laboriosam atque asperiores iusto autem voluptate
laborum, mollitia pariatur aliquam deleniti consequuntur error veniam
nulla vel et unde quae aut sed culpa sapiente.
</p>
</div>
</>
)
}
export default Appimport { useState, useEffect } from "react"
import BtnStyleSwitcher from "./components/BtnStyleSwitcher"
function App() {
const [mode, setMode] = useState(true)
useEffect(() => {
document.documentElement.classList.toggle("darkmode", mode)
/*
Any code for e.g., local storage would go here...
*/
}, [mode])
function handleMode() {
setMode((prevMode) => !prevMode)
}
return (
<>
<BtnStyleSwitcher
handleClick={handleMode}
mode={mode}
/>
<div className="content">
<p>
Page content ... ipsum dolor sit amet consectetur adipisicing elit.
Eaque impedit repudiandae necessitatibus sequi accusamus unde sed
animi similique, quia maxime alias nihil nesciunt? Incidunt dolorem
cum deserunt, laboriosam atque asperiores iusto autem voluptate
laborum, mollitia pariatur aliquam deleniti consequuntur error veniam
nulla vel et unde quae aut sed culpa sapiente.
</p>
</div>
</>
)
}
export default Appimport PropTypes from "prop-types"
import { MdDarkMode, MdLightMode } from "react-icons/md"
function BtnStyleSwitcher({ handleClick, mode }) {
return (
<button
type="button"
onClick={handleClick}
aria-pressed={mode ? "true" : "false"}
aria-label="Toggle dark mode"
>
{mode ? (
<MdDarkMode aria-hidden="true" />
) : (
<MdLightMode aria-hidden="true" />
)}
<span>Darkmode: {mode ? "on" : "off"}</span>
</button>
)
}
BtnStyleSwitcher.propTypes = {
handleClick: PropTypes.func.isRequired,
mode: PropTypes.bool,
}
export default BtnStyleSwitcherimport { useState, useEffect } from "react"
/**
- Data source: "https://some-server/items"
- Data structure:
{
items: [
{
"id": "1",
"title": "Title #1",
"imageUrlWebp": "https://images.com/img1.webp",
"imageUrlPng": "https://images.com/img1.png",
"width": 200,
"height": 200,
"description": "Description #1",
},
{
"id": "2",
"title": "Title #2",
"imageUrlWebp": "https://images.com/img2.webp",
"imageUrlPng": "https://images.com/img2.png",
"width": 200,
"height": 200,
"description": "Description #2",
},
etc,
],
}
*/
function Items() {
const [items, setItems] = useState([])
useEffect(() => {
async function getItems() {
try {
const res = await fetch("https://some-server/items")
const itemsData = await res.json()
setItems(itemsData.items)
} catch (error) {
console.log(error)
}
}
getItems()
}, [])
const itemsList = items.map((item) => {
return (
<li key={item.id}>
<h2>{item.title}</h2>
<picture>
<source
srcSet={item.imageUrlWebp}
type="image/webp"
/>
<img
src={item.imageUrlPng}
alt={item.name}
loading="lazy"
width="200"
height="200"
/>
</picture>
<p>{item.description}</p>
</li>
)
})
return (
<>
<h1>Items</h1>
{items ? <ul>{itemsList}</ul> : "Loading ..."}
</>
)
}
export default Itemsimport PropTypes from "prop-types"
function Loading({ title }) {
return (
<>
<p className="visually-hidden">Loading {title}...</p>
<div
className="loading"
aria-hidden="true"
>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</>
)
}
Loading.propTypes = {
title: PropTypes.string,
}
export default Loading<Loading title="Loading..." />.loading {
position: relative;
width: 80px;
height: 80px;
margin-inline: auto;
}
.loading div {
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid currentColor;
border-radius: 50%;
animation: loading 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: currentColor transparent transparent transparent;
}
.loading div:nth-child(1) {
animation-delay: -0.45s;
}
.loading div:nth-child(2) {
animation-delay: -0.3s;
}
.loading div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Screenreader only */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(0);
border: 0;
}
/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
html,
html:focus-within {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
transition-delay: 0ms !important;
}
}Src: Rohit Yadav - Adding Page Titles to React App
This sets the <title> in index.html per component.
import PropTypes from "prop-types"
import { useEffect } from "react"
import { useLocation } from "react-router-dom"
const PageTitle = ({ title }) => {
const location = useLocation()
useEffect(() => {
document.title = title
}, [location, title])
return null
}
PageTitle.propTypes = {
title: PropTypes.string,
}
export default PageTitleimport { BrowserRouter, Routes, Route } from "react-router-dom"
import Home from "./pages/Home"
import About from "./pages/About"
import PageTitle from "./components/PageTitle"
import "./server"
function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<>
<PageTitle title="Home page" />
<Home />
</>
}
/>
<Route
path="/about"
element={
<>
<PageTitle title="About page" />
<About />
</>
}
/>
</Routes>
</BrowserRouter>
)
}
export default App> [!NOTE]
> Highlights information that users should take into account, even when skimming.
> [!TIP]
> Optional information to help a user be more successful.
> [!IMPORTANT]
> Crucial information necessary for users to succeed.
> [!WARNING]
> Critical content demanding immediate user attention due to potential risks.
> [!CAUTION]
> Negative potential consequences of an action.
Note
Highlights information that users should take into account, even when skimming.
Tip
Optional information to help a user be more successful.
Important
Crucial information necessary for users to succeed.
Warning
Critical content demanding immediate user attention due to potential risks.
Caution
Negative potential consequences of an action.

git pull(This will pull down any changes topackage.jsonandpackage-lock.json).- Update
deploy.ymlto the correct version (currently v.4) git add .git commit -m "Updated deploy.yml to version 4"git push
- Under "Actions", check that the update action has been successful. Then,
- if the "Pages build and deployment" action takes place automatically, check that the corresponding Git Page is displaying correctly after the action has finished.
- Else if the "Pages build ..." action has not happened automatically:
- Under "Settings > Pages > Branch", change "gh-pages" to "None" and click "Save".
- Go to the corresponding Git Page and check that it is now "404".
- Go back to "Settings > Pages > Branch", change "None" to "gh-pages" and click "Save".
- Under "Actions" check that "pages build and deployment" action has been successful.
- Finally, check that the corresponding Git Page is displaying correctly.
Say I've got both a git repository (called my-git-repo) on Github, and a local copy synched with the remote version.
- I will be working on the local repository.
- I need to do some experimental work on the index.html file (the only file in the repository).
Therefore, I want to:
- Switch to a new branch (called 'experiment')
- Make changes on index.html in branch 'experiment' until I am satisfied with it.
- Merge 'experiment' branch with main branch.
- Delete 'experiment' branch.
- Push main branch to 'my-git-repo' on GitHub.
git checkout -b experiment
git add index.html
git commit -m "Experimental changes to index.html"
git checkout main
git merge experiment
git branch -d experiment
git push
If you delete everything except the .git folder, you're still inside a Git repository — just with the working directory empty.
You can then restore everything by doing:
git reset --hard origin/main
This command tells Git to:
-
Reset the local branch (main) to match the remote (origin/main)
-
Overwrite the working directory to reflect that (bringing back index.html)
After deleting the folder, run:
cd my-projects
git clone https://github.com/your-username/my-git-repo.git name-of-project
You'll get a fresh local copy of the entire repository as it exists on GitHub - including all branches (though you'll be on the default branch, usually main, by default).
I get the following conflict:
<<<<<< HEAD
<h1>Main version</h1>
=======
<h1>Experimental version</h1>
>>>>>> experiment
I want <h1>Experimental version</h1> to be the result of resolving the conflict.
Change this:
<<<<<< HEAD
<h1>Main version</h1>
=======
<h1>Experimental version</h1>
>>>>>> experiment
To this:
<h1>Experimental version</h1>
Then stage and commit the resolved file in which it sits (e.g. index.html):
git add index.html
git commit
Instead of publishing from the project root, use the /docs folder as the GitHub Pages source.
This allows the site to be built using minified, transpiled JavaScript and minified, flattened nested CSS, while keeping the unprocessed source files in the root.
-
Install esbuild:
npm install --save-dev esbuild
-
Then run:
npx esbuild index.js --bundle --minify --target=es2015 --outfile=docs/bundle.js
-
Install PostCSS and plugins:
npm install --save-dev postcss postcss-nesting postcss-cli cssnano
-
Create a
postcss.config.jsfile in the project root:module.exports = { plugins: [ require("postcss-nesting"), require("cssnano")({ preset: "default" }), ], }
-
Then run:
npx postcss style.css --output docs/style.min.css
In all HTML files:
-
Update the script tag:
<!-- Before --> <script type="module" src="./index.js" ></script> <!-- After --> <script src="./bundle.js" defer ></script>
-
Update the CSS link:
<!-- Before --> <link rel="stylesheet" href="./style.css" /> <!-- After --> <link rel="stylesheet" href="./style.min.css" />
[!WARNING] > Do not copy
README.md,LICENSE,.gitignore, or any build/dev files into/docs.
-
Go to:
https://github.com/[yourname]/colour-contrast-checker/settings/pages -
Under Build and deployment > Source, select:
- Branch:
main(or your default branch) - Folder:
/docs
- Branch:
-
Save and wait for the site to deploy.
All snippets tested on Windows 10 with:
- Chrome
- Firefox
- Microsoft Edge
Each snippet tested in both browser and device views.
git pull
[!NOTE] >
npm installinstalls dependencies frompackage.json, may updatepackage-lock.json, and preserves existingnode_modules, whilenpm cistrictly installs exactly what's inpackage-lock.jsonafter deletingnode_modules.
# This will run whether node_modules is installed or not.
Remove-Item -Recurse -Force node_modules -ErrorAction SilentlyContinue
# installs node_modules / dependencies exactly as specified in package-lock.json
npm cinpm audit fix
npm run dev- Open
http://localhost:5173and verify site works - Press
Ctrl+Cwhen done
npm run buildgit status
git add .
git commit -m "Update package-lock.json after PR merge"
git push
Remove-Item -Recurse -Force node_modules -ErrorAction SilentlyContinueipconfig | findstr /C:Addressipconfig /flushdnsRemove-Item -Recurse -Force node_modules -ErrorAction SilentlyContinueSay you have this file structure in the root directory of your project:
index.html
index.js
js-modules/
You want to bundle all JavaScript modules — including index.js — into a single file (e.g. output.js).
const esbuild = require("esbuild")
// Build the journal project into one minified file
esbuild
.build({
entryPoints: ["./index.js"], // your main module
bundle: true, // follow all imports
minify: true, // remove comments and whitespace
outfile: "./output.js", // output file in root
sourcemap: false, // optional: no source map
format: "iife", // wraps in an immediately-invoked function
})
.catch(() => process.exit(1))
console.log("Build complete: output.js created.")- Save this file in the root of your project, next to
index.js.
- In a terminal, run:
npm init -y
npm install esbuild --save-dev
This installs esbuild and creates a minimal package.json.
- In a terminal, run:
node build.jsA compact, minified JavaScript file named output.js will be created in the project root.
- In your HTML, replace:
<script src="./index.js" type="module"></script>
- With:
<script src="./output.js" defer></script>
- Then launch the app by double-clicking
index.html(or opening it from Finder/Explorer).
If everything works, you may safely delete:
build.jsnode_modulespackage.jsonpackage-lock.jsonindex.jsjs-modules(all modules are now inoutput.js)
Your project now runs entirely from a single self-contained JavaScript file.
Windows + I> Settings > 2. Accounts > Family & other users.- Click 'Add someone else to this PC'.
- Choose:
- 'I don't have this person's sign-in information'.
- Then 'Add a user without a Microsoft Account'.
- Name it something like 'TestUser' and leave the password field blank.
- Log out of the current user ('YourName'):
Ctrl + Alt + Delete> Sign out.
You will now be on the login screen. To log in to 'TestUser':
- Click the 'TestUser' icon.
Note
The laptop virtual keyboard might obscure the user icons. Press e.g. Esc and the keyboard should disappear.
Various setup processes will take place. Say no to the Microsoft options that want you to submit data to it, etc. Once setup is over, do the tests.
There are 2 ways of doing this:
Ctrl + Alt + Delete> Sign out.
You will now be on the login screen. To log in to 'YourName':
- Click the 'YourName' icon.
Note
The laptop virtual keyboard might obscure the user icons. Press e.g. Esc and the keyboard should disappear.
The downside of signing out is that Gmail, Instagram, Amazon, etc. will all be signed-out.
The upside is that it is very easy to now delete the 'TestUser' account.
Ctrl + Alt + Delete> Switch user.
You will now be on the login screen. To log in to 'YourName':
- Click the 'YourName' icon.
Note
The laptop virtual keyboard might obscure the user icons. Press e.g. Esc and the keyboard should disappear.
The upside of switching users is that Gmail, Instagram, Amazon, etc. will all still be signed-in.
The downside is that it is not as easy to delete the 'TestUser' account.
If you returned to your main user profile 'YourName' via:
-
Sign out:
Windows + I> Settings > 2. Accounts > Family & other users.- Click on the 'TestUser' icon.
- Click 'Remove'.
-
Switch user:
- Restart your PC and power on again.
Windows + I> Settings > 2. Accounts > Family & other users.- Click on the 'TestUser' icon.
- Click 'Remove'.
Step 1 — Open Command Prompt as Administrator
- Press Win + S, type
cmd - Right-click Command Prompt → Run as administrator
Step 2 — Run the combined repair command
DISM.exe /Online /Cleanup-Image /RestoreHealth && sfc /scannow
Explanation:
DISM.exe /Online /Cleanup-Image /RestoreHealth
Checks and repairs the Windows component store, downloading fresh files if needed.&&means the next command will run only if the previous one succeeds.sfc /scannow
Scans and repairs system files using the healthy copies ensured by DISM.
Step 3 — Reboot after completion
Some fixes take effect only after restarting.
Optional — Check logs
- DISM log:
C:\Windows\Logs\DISM\dism.log - SFC log (search for "[SR]"):
C:\Windows\Logs\CBS\CBS.log
If DISM fails with an error such as being unable to download files, try:
Ensure you are connected to the internet and try again.
- Download the correct Windows 10 ISO from Microsoft.
- Mount it (right-click → Mount).
- Note the drive letter (e.g.,
E:). - Run:
DISM.exe /Online /Cleanup-Image /RestoreHealth /Source:E:\sources\install.wim /LimitAccess
Replace E: with your ISO’s drive letter.
Tip: If install.wim is not present but install.esd is, the command changes to:
DISM.exe /Online /Cleanup-Image /RestoreHealth /Source:esd:E:\sources\install.esd:1 /LimitAccess
- After DISM completes successfully, run:
sfc /scannow
- Type Disk Clean-up in the Search bar.
- Click Clean-up system files button.
- After scan complete, click More Options tab.
- Under System Restore and Shadow Copies click Clean-up button.
- When pop-up appears, click Delete.
- Click "OK" button in main screen.
- When pop-up appears, click Delete Files.
- Run PowerShell as Adminstrator.
- Paste the following commands (in one block, or separately) into the PowerShell window:
Remove-Item -Path "C:\Windows\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinue
vssadmin delete shadows /for=C: /oldest /quiet
Write-Host "Cleanup of Windows Update files and shadow copies complete!"
- Once completed, you'll get the message, "Cleanup of Windows Update files and shadow copies complete!".
Event Viewer > Windows Logs > System > Filter Current Log: ID 17
- Note total number of events.
- Focus on sudden changes or patterns, not the absolute number.
- Occasional spikes after BIOS/driver updates are normal.
- Open as admin
- Run (then Enter):
# Check for uncorrected hardware errors in the last 30 days
$since = (Get-Date).AddDays(-30)
$errors = Get-WinEvent -FilterHashtable @{LogName='System'; StartTime=$since} |
Where-Object { ($_.Id -in 18,19) -and ($_.LevelDisplayName -in 'Error','Critical') } |
Select-Object TimeCreated, Id, ProviderName, LevelDisplayName, Message |
Sort-Object TimeCreated -Descending
if ($errors) {
Write-Output "⚠️ Uncorrected hardware errors detected:"
$errors
Write-Output "System log will NOT be cleared. Investigate errors first."
} else {
Write-Output "No uncorrected hardware errors found in the last 30 days — system is healthy ✅"
$confirmClear = Read-Host "Do you want to clear the System log? (Y/N)"
if ($confirmClear -match '^[Yy]$') {
Write-Output "Clearing System log..."
Wevtutil cl System
Write-Output "System log cleared ✅"
} else {
Write-Output "System log not cleared."
}
}When working on multi-step or ongoing projects with ChatGPT, clear and consistent communication helps it deliver the best results while minimizing mistakes or "hallucinations." Some tips to keep tips on track:
- Share relevant code, specs, or prior details upfront, especially if continuing from a previous conversation.
- If the conversation is long, consider quoting the key part or code snippet that ChatGPT should focus on.
Since you cannot see message numbers or timestamps, refer to earlier messages by quoting unique phrases, distinctive code snippets, or describing the content clearly.
Example:
"Please use the
name-of-file.jscode I shared with the exported functions:function_1,function_2, etc."
This helps ChatGPT locate and apply the exact content you want it to use.
Break down big blocks of code, specs, or requests into smaller, manageable chunks.
Number the chunks or label them clearly (e.g., "Chunk 1," "Chunk 2") so ChatGPT can process and respond step-by-step.
Clearly state what you want ChatGPT to do with each chunk or piece of info (e.g., summarize, refactor, explain, integrate).
If you want the AI to avoid assumptions or hallucinations, explicitly say so.
When continuing across multiple messages, remind ChatGPT what you have both agreed on or what to prioritize.
Example:
"Remember to only use the code I gave you in Chunk 3 for the [topic] helpers."
If you spot an error or hallucination, point it out quickly and provide the correct info to reset the context.
This helps avoid carrying forward mistakes.
Because the ChatGPT interface does not show message numbers or timestamps, the best way to reference a previous message is by:
- Quoting a unique phrase or code snippet from that message.
- Describing the content clearly, for example:
"In the message where I shared the full
name-of-file.jswith the four functions…"
- Using distinctive keywords or content that uniquely identify that part of the conversation.
This way, ChatGPT can search its internal context to find exactly what you mean and avoid confusion.
- ChatGPT can search its context for those keywords or code blocks.
- It reduces ambiguity and helps ChatGPT find the right part of the conversation internally.
- Since you cannot see system-generated IDs, referencing by content is the most reliable method.
If you want a quick way to help ChatGPT pinpoint a section, you can copy-paste a short snippet or unique line from the message you want it to reference. That way, it is sure exactly what you mean.
Often, when typing into the chat input field, I can only type one letter at a time; or, letters and words appear some time after they have been typed.
I assume this is because so many people are accessing the bot that the server slows down.
An easy solution to this is to type a message into e.g. "Notepad" first, then paste it into the chat input field.


