- 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-shadow
applied to only one side - Invert image (darkmode / lightmode)
- Set Multiple Attributes
- 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
.isConnected
then.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
- 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
index
Parameter of.map()
Method to Supply Component'skey
Value - 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 await
and.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
/docs
after bundling JS and minifying CSS
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>
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
em
rather thanrem
is 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+P
orCmd+Shift+P
, type "convert px to rem", then hit theEnter
key.
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 item
s 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: 0
allows themain
element to shrink below its content width, thereby allowing the.items
grid 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));
}
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 checked
const checkboxes = [
{ checked: false },
{ checked: true },
{ checked: true },
]
const anyChecked = checkboxes.some(box => box.checked)
console.log(anyChecked) // true, because at least one checkbox is checked
const 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 myVar
index.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
let
orvar
. - 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
<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: none
to 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
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()
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 Image
import 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 Component
import 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 ComponentParent
import 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 Button
import 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 App
When 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 Parent
import 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 Child
In 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 App
export 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 LogRandomUrls
import { 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 Items
import { useState } from "react"
function Contact() {
const [contact, setContact] = useState({
firstName: "John",
lastName: "Doe",
phone: "+44 (207) 391 4023",
email: "[email protected]",
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 Contact
import { 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.innerWidth
is displayed in theh1
. - When it is toggled to
false
, theh1
is hidden. - However, if the clean up function is not included (in this case, removing the event listener)
window.innerWidth
will 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 App
import { 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 App
import 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 BtnStyleSwitcher
import { 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 Items
import 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 PageTitle
import { 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.json
andpackage-lock.json
).- Update
deploy.yml
to 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.js
file 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.
ipconfig | findstr /C:Address
ipconfig /flushdns
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
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.js
code 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.js
with 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.