Devices Breakpoint
A popular strategy is to do mobile-first development with the following breakpoints:
- Mobile-first : base style is designed for small devices and most phones ( < 480px )
@media (min-width: 480px): for portrait tablets ( < 1024px )@media (min-width: 1024px): for desktop ( ≥ 1024px )
Sample desktop-first breakpoints:
- Desktop-first : base style is for desktop ( > 1024px )
@media (max-width: 1024px): for portrait tablets ( > 480px )@media (max-width: 480px): for most phones ( ≤ 480px )
A desktop-first design, and mobile-first development approach could work. If experimenting with flexbox or grid, then a desktop-first approach might be better.
Useful Properties:
overflow-wrap: break-wordandwhite-space: pre-wrapare useful for handling overflowing text like in <code>- Be careful of using
min-widthfor large screen size, as smaller screen sizes wantsmin-width: 0
Scaling Images
- Use vector images,
svg, to handle various screen sizes- Put them in <img> tag or its own <svg> tag
- For raster images, use
sizesandsrcsetattributes on the elementimgto let the browser decide what image size to pick<img src='assets/image.jpg' srcset=' thumbs/image.jpg 480w, mediums/image.jpg 1024w, assets/image.jpg 1920w' sizes='(max-width: 480px) 480px, (max-width: 1024px) 1024px, 1920px' alt='Responsive Image'> - Use
<picture>element with<sourcesif completely different image or switch format is needed between screen sizes
Using flex-direction & flex-basis
flex-direction: row and flex-direction: column can be use to switch between horizontal and vertical main-layout
.flexResponsiveContainer {
display: flex;
flex-direction: row;
background-color: var(--contrastColor1);
}
.flexContainer > div {
flex-basis: 25%; /* Makes 4 equal columns */
margin: 10px;
text-align: center;
line-height: 50px;
background-color: #f1f1f1;
}
@media (max-width: 800px) {
/* Responsive layout - makes a one column layout instead of a 4-column layout */
.flexResponsiveContainer {
flex-direction: column;
}
}Using flex-wrap & flex-basis
flex-basis in children elements to change the number of columns, andflex-wrap: wrap in the parent container when in vertical layout.
.flexContainer {
display: flex;
background-color: var(--contrastColor1);
}
.flexContainer > div {
flex-basis: 50%; /* Makes 2 equal columns */
margin: 10px;
text-align: center;
line-height: 50px;
background-color: #f1f1f1;
}
@media (max-width: 800px) {
/* Responsive layout - makes a one column layout instead of a 2-column layout */
.flexContainer {
flex-wrap: wrap;
}
.flexContainer > div {
flex-basis: 100%;
}
}Changing Theme
Add and manage a data-theme attribute using setAttribute(),getAttribute, and localStorage to change the whole site state
- Set attribute on
document.documentElementto apply attribute on the root level or whole site
// Calls when the page first load
export function getTheme() {
const theme = localStorage.getItem('data-theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
}
// Calls when the switch theme button is clicked
function toggleTheme() {
if(document.documentElement.getAttribute('data-theme') === 'dark'){
document.documentElement.setAttribute('data-theme', 'light');
localStorage.setItem('data-theme', 'light');
} else {
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem('data-theme', 'dark');
}
}Then, in the CSS file, change the CSS variables inside the declaration block with CSS selector picking elements containing the attribute and value [data-theme="dark"]
:root {
--primary-color: #333;
--secondary-color: #f4f4f4;
}
/* Dark Theme */
[data-theme='dark'] {
--primary-color: #111;
--secondary-color: #1b1b1b;
}Dropdown Menu on Mobile
Navigation bar will switch to a drop down menu if the screen is narrow enough.
Keypoints:
- Use
position: relativeon the dropdown container to make the dropdown content position relative to this element - Use
position: absoluteon the dropdown content to position it relative to the dropdown container - Use
top: 100%; left: 0; right: 0;to position the dropdown content below the dropdown button - Use
top: 0; left: 100%;to position the dropdown content to the right of the dropdown button - Use
opacity: 0andopacity: 1to hide/show elements for animation/transition instead ofdisplay: none - On mobile, hover doesn't work, so the dropdown button has to be bound to an onClick attribute and the function will toggle the dropdown content
<div class='navbar'>
<a href='#home'>Long Home</a>
<a href='#news'>Long News</a>
<a href='#about'>Long About</a>
<a href='#contact'>Long Contact</a>
<div class='dropdownBtn' onClick='showDropdown()'>
<div>Dropdown Menu ▼</div>
<div class='dropdown-content' id='dropdownContent'>
<a href='#home'>Home</a>
<a href='#news'>News</a>
<a href='#about'>About</a>
<a href='#contact'>Contact</a>
</div>
</div>
</div>
<script>
function showDropdown() {
document.getElementById('dropdownContent').classList.toggle('open');
}
</script>.navbar {
display: flex;
justify-content: flex-start;
align-items: center;
}
/* Links inside the navbar */
.navbar a {
flex-basis: 25%; /* To split the 4 menu links evenly */
}
/* The dropdown container */
.dropdown {
display: none; /* Initially hide the dropdown menu */
background-color: inherit;
}
.navbar a, .dropdown{
color: white;
font-size: 16px;
text-decoration: none;
text-align: center;
padding: 14px 16px; /* To fill up the menu link's space */
}
/* Add a red background color to navbar links on hover ,and active for mobile */
.navbar a:hover, .nav a:active,
.dropdownBtn:hover, .dropdownBtn:active {
background-color: var(--nav-hover-bg-color);
}
@media (max-width: 800px) {
/* Switch to dropdown menu on mobile */
.navbar > a {
display: none;
}
.dropdown {
display: flex;
flex-basis: 25%;
/* use position: relative to make .dropdown-content relative to this element*/
position: relative;
}
/* Dropdown content (hidden by default) */
.dropdown-content {
display: none;
position: absolute; /* makes it relative to the .dropdown element */
left: 0;
right: 0; /* Make the dropdown content take the full width of .dropdown */
top: 100%; /* Position the dropdown content below the dropdown menu */
background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
/* Show the dropdown-content when hovering/clicking dropdownBtn */
.dropdownBtn:hover .dropdown-content, .dropdown-content.open {
display: block;
}
/* Links inside the dropdown */
.dropdown-content a {
display: block;
color: black;
padding: 12px 16px; /* To fill up the dropdown link's space */
}
/* Add a grey background color to dropdown links on hover */
.dropdown-content a:hover {
background-color: #ddd;
}
}Masonry Gallery using column-count
Only works well all the images are loaded at once, because they are added from top to bottom instead of left to right. It falls apart if images are loaded dynamically.
Keypoints:
- Use
display: blockandcolumn-count: xto adjust the number of columns break-inside: avoid;is also needed to prevent caption of images from splitting across columns- Use
@media (max-width: 800px)to change column-count when the screen narrows
<div className='gallery'>
<div>
<img>
</div>
<div>
<img>
</div>
...
</div>/* Create four equal columns that sits next to each other; */
.gallery {
display: block;
column-count: 4;
column-gap: 10px;
}
.gallery div {
display: block;
break-inside: avoid; /* To prevent part of the div element from wrapping to the next column */
margin-bottom: 4px;
}
.gallery img {
width: 100%;
}
/* Responsive layout - makes a single column-layout */
@media (max-width: 480px) {
.gallery {
column-count: 2;
}
}Masonry Gallery using Flex Box
Keypoints :
- Use
display: flexandflex-basisto generate columns, then each image is placed in the shortest column - Cheaper than
grid, but can have empty gaps in columns after resizing - Good if not strict with responsiveness
- Number of <div> columns need to be calculated on page load, if the browser try to wrap the columns, then new images will load at the top columsn as well
- If screen changes columns will need to be merged/split and items would need to be moved to maintain left to right ordering
<div className='row' id='galleryFlexBox'>
<div className='column'>
<div> <img> </div>
<div> <img> </div>
</div>
<div className='column'>...</div>
<div className='column'>...</div>
<div className='column'>...</div>
</div>* {
box-sizing: border-box;
}
/* The main container for the gallery, the whole page is one row */
.row {
display: flex;
flex-wrap: wrap; // Require to make the container responsive on resize
gap: 5px;
}
/* Create four equal columns that sits next to each other */
.column {
flex: 1 1 25%;
}
.column img {
width: 100%;
}
/* Responsive layout - make 2 column layout*/
@media (max-width: 480px) {
.column {
flex: 49%;
}
}Masonry Gallery using Grid
Keypoints :
- Use
gridand calculategrid-row-end, the row span, for each image - Setting the row gap to 0 will make it easier to calculate
- Lower
grid-auto-rowsvalue means shorter row-track height and more accurate grid - Make sure to recalculate the grid items on finished resource loading and screen resize,
'load'and'resize' - Expensive for large gallery because the browser has to recalculate every image everytime there's a resize
- Using
loading='lazy'on <img> can improve performance for large galleries.
<div className='grid' id='galleryGrid'>
<div> <img> </div>
<div> <img> </div>
...
</div>.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* auto fill column number, min-width of 200px and can grow evenly if there space */
grid-auto-rows: 10px; /* Height of each row track, the lower the number the more accurate the grid */
gap: 0 10px; /* Set row track gap to 0 to make calculation easier */
}
.item {
margin-bottom: 5px;
break-inside: avoid; /* So caption doesn't wrap onto a new column */
}
/* Responsive layout - keep 2 columns for mobile screen */
@media (max-width: 480px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
.item {
margin-bottom: 2px;
}
.main-content {
padding: 1px; /* lower padding to get more screen space */
}
}/*
* Calculate the row span needed for each item
* The browser has a LIMIT of 16,000 rows track for a grid.
* Increase grid-auto-rows if row limit is reached
*/
function calcGridItem(item) {
const grid = document.getElementById(gridId);
const img = item.querySelector('img');
/* Goal: calculate how many row-track the item will span (S)
* Think of grid as cell-shading, where you have to identify how many rows the picture will take up on the canvas
* Row-track: the invisible row in the grid that is used
* rowTrackHeight: the height of one row-track
* rowTrackGap: the gap between each row-track (i.e. bottom margin of row-track)*/
// Getting all values for item's total height
const itemBottomMargin = parseInt(window.getComputedStyle(item).getPropertyValue('margin-bottom'));
const imgHeight = getRenderedHeight(img, grid);
// Getting values for total row-track height
const rowTrackHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
const rowTrackGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap'));
/* The number of rows an item will span (S) is:
* S = TI / TR
* TI = Total Item Height = Image_Rendered_Height + Item_Bottom_Margin
* TR = Total Row Track-Height = Row_Track_Height + Row_Track_Gap
*/
// Calculating the total item height and row span
const itemHeight = imgHeight + itemBottomMargin;
const rowSpan = parseInt(Math.ceil(itemHeight / (rowTrackHeight + rowTrackGap)));
// Setting the row span on the item
item.style.gridRowEnd = 'span ' + rowSpan;
}
// Recalculate the items when all resource/images finished loading or when the screen resizes
const events = ['load', 'resize'];
events.forEach(function (event) {
window.addEventListener(event, recalcAllGridItems);
});