The lang attribute can only have one language associated with it. This means
the <html>
attribute can only have one language, even if there are multiple
languages on the page. Set lang
to the primary language of the page.
<html lang="ar,en,fr,pt">...</html>
<html lang="ar">...</html>
Links
Similar to buttons, links primarily get their accessible name from their text content. A nice trick when creating a link is to put the most meaningful piece of text into the link itself, rather than filler words like "Here" or "Read More."
Check out our guide to web performance <a href="/guide">here</a>.
Check out <a href="/guide">our guide to web performance</a>.
Check if an animation triggers layout
An animation that moves an element using something other than transform
, is likely to be slow.
In the following example, I have achieved the same visual result animating top
and left
, and using transform
.
.box { position: absolute; top: 10px; left: 10px; animation: move 3s ease infinite; } @keyframes move { 50% { top: calc(90vh - 160px); left: calc(90vw - 200px); } }
.box { position: absolute; top: 10px; left: 10px; animation: move 3s ease infinite; } @keyframes move { 50% { transform: translate(calc(90vw - 200px), calc(90vh - 160px)); } }
You can test this in the following two Glitch examples, and explore performance using DevTools.
With the same markup, we can replace: padding-top: 56.25%
with aspect-ratio: 16 / 9
, setting
aspect-ratio
to a specified ratio of width
/ height
.
.container { width: 100%; padding-top: 56.25%; }
.container { width: 100%; aspect-ratio: 16 / 9; }
Using aspect-ratio
instead of padding-top
is much more clear, and does not overhaul the padding
property to do something outside of its usual scope.
Yeah, that's right, I'm using reduce
to chain a sequence of promises. I'm so
smart. But this is a bit of so smart coding you're better off without.
However, when converting the above to an async function, it's tempting to go too sequential:
async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } }
function markHandled(...promises) { Promise.allSettled(promises); } async function logInOrder(urls) { // fetch all the URLs in parallel const textPromises = urls.map(async (url) => { const response = await fetch(url); return response.text(); }); markHandled(...textPromises); // log them in sequence for (const textPromise of textPromises) { console.log(await textPromise); } }
Writing Houdini custom properties
Here's an example of setting a custom property (think: CSS variable), but now
with a syntax (type), initial value (fallback), and inheritance boolean (does
it inherit the value from its parent or not?). The current way to do this is
through CSS.registerProperty()
in JavaScript, but in Chromium 85 and later, the
@property
syntax will be supported in your CSS files:
CSS.registerProperty({ name: '--colorPrimary', syntax: '' , initialValue: 'magenta', inherits: false });
@property --colorPrimary { syntax: '' ; initial-value: magenta; inherits: false; }
Now you can access --colorPrimary
like any other CSS custom property, via
var(--colorPrimary)
. However, the difference here is that --colorPrimary
isn't
just read as a string. It has data!
CSS backdrop-filter
applies one or more effects to an element that is translucent or transparent. To understand that, consider the images below.
.frosty-glass-pane { backdrop-filter: blur(2px); }
.frosty-glass-pane { opacity: .9; backdrop-filter: blur(2px); }
The image on the left shows how overlapping elements would be rendered if backdrop-filter
were not used or supported. The image on the right applies a blurring effect using backdrop-filter
. Notice that it uses opacity
in addition to backdrop-filter
. Without opacity
, there would be nothing to apply blurring to. It almost goes without saying that if opacity
is set to 1
(fully opaque) there will be no effect on the background.
Unlike the unload
event, however, there are legitimate uses for
beforeunload
. For example, when you want to warn the user that they have
unsaved changes they'll lose if they leave the page. In this case, it's
recommended that you only add beforeunload
listeners when a user has unsaved
changes and then remove them immediately after the unsaved changes are saved.
window.addEventListener('beforeunload', (event) => { if (pageHasUnsavedChanges()) { event.preventDefault(); return event.returnValue = 'Are you sure you want to exit?'; } });
function beforeUnloadListener(event) { event.preventDefault(); return event.returnValue = 'Are you sure you want to exit?'; }; // A function that invokes a callback when the page has unsaved changes. onPageHasUnsavedChanges(() => { window.addEventListener('beforeunload', beforeUnloadListener); }); // A function that invokes a callback when the page's unsaved changes are resolved. onAllChangesSaved(() => { window.removeEventListener('beforeunload', beforeUnloadListener); });
Minimize use of Cache-Control: no-store
Cache-Control: no-store
is an HTTP header web servers can set on responses that instructs the browser not to store the response in any HTTP cache. This should be used for resources containing sensitive user information, for example pages behind a login.
The fieldset
element, which contains each input group (.fieldset-item
), is using gap: 1px
to
create the hairline borders between elements. No tricky border solution!
.grid { display: grid; gap: 1px; background: var(--bg-surface-1); & > .fieldset-item { background: var(--bg-surface-2); } }
.grid { display: grid; & > .fieldset-item { background: var(--bg-surface-2); &:not(:last-child) { border-bottom: 1px solid var(--bg-surface-1); } } }
Natural grid wrapping
The most complex layout ended up being the macro layout, the logical layout
system between <main>
and <form>
.
<input type="checkbox" id="text-notifications" name="text-notifications" >
<label for="text-notifications"> <h3>Text Messages</h3> <small>Get notified about all text messages sent to your device</small> </label>
The fieldset
element, which contains each input group (.fieldset-item
), is using gap: 1px
to
create the hairline borders between elements. No tricky border solution!
.grid { display: grid; gap: 1px; background: var(--bg-surface-1); & > .fieldset-item { background: var(--bg-surface-2); } }
.grid { display: grid; & > .fieldset-item { background: var(--bg-surface-2); &:not(:last-child) { border-bottom: 1px solid var(--bg-surface-1); } } }
Tabs <header>
layout
The next layout is nearly the same: I use flex to create vertical ordering.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
The .snap-indicator
should travel horizontally with the group of links, and
this header layout helps set that stage. No absolute positioned elements here!
Gentle Flex is a truer centering-only strategy. It's soft and gentle, because
unlike place-content: center
, no children's box sizes are changed during the
centering. As gently as possible, all items are stacked, centered, and spaced.
.gentle-flex {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1ch;
}
- Only handles alignment, direction, and distribution
- Edits and maintenance are all in one spot
- Gap guarantees equal spacing amongst n children
- Most lines of code
Great for both macro and micro layouts.
Usage
gap
accepts any CSS length
or percentage as a value.
.gap-example {
display: grid;
gap: 10px;
gap: 2ch;
gap: 5%;
gap: 1em;
gap: 3vmax;
}
Gap can be passed 1 length, which will be used for both row and column.
.grid { display: grid; gap: 10px; }
.grid { display: grid; row-gap: 10px; column-gap: 10px; }
Gap can be passed 2 lengths, which will be used for row and column.
.grid { display: grid; gap: 10px 5%; }
.grid { display: grid; row-gap: 10px; column-gap: 5%; }