Front End Web Development

CSS-Questions

Css Tricks - Tue, 08/12/2025 - 5:02am

Sunkanmi Fafowora is a frequent flier around here. You’ve probably seen his name pop up in the CSS-Tricks Almanac and we actually just published something today that he wrote up for the color-mix() function. The guy spends a lot of time in the Almanac because he loves technical documentation, something he showed off when writing the CSS Color Functions Guide.

And it’s that love for technical documentation that lead him to ship CSS-Questions (gotta love that hyphenated URL, right?!), a place where you can test your CSS knowledge with over 100 questions. You can take the comprehensive exam or a basic one with 20 questions if all you want is a pop quiz.

And of course, the first question I get is related to CSS color functions!

You’re just trolling me now, aren’t you Sunkanmi?!

My first try was a valiant effort, I’d say…

Almost perfect. Could be the title of my autobiography.

There, now I feel better. 😅

CSS-Questions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Getting Creative With Quotes

Css Tricks - Mon, 08/11/2025 - 3:43am

Block quotes and pull quotes are useful for punctuating solid blocks of running text. They’re also two of the best typographic elements for acting as visual landmarks to catch someone’s eye. There are no rules about how long a quote should be, how big it should look, or even how it’s styled.

So, how do you design block quotes and pull quotes to reflect a brand’s visual identity and help tell its story? Here’s how I do it by styling the HTML blockquote element using borders, decorative quote marks, custom shapes, and a few unexpected properties.

Patty Meltt is an up-and-coming country music sensation.

My brief: Patty Meltt is an up-and-coming country music sensation, and she needed a website to launch her new album. She wanted it to be distinctive-looking and memorable, so she called Stuff & Nonsense. Patty’s not real, but the challenges of designing and developing sites like hers are.

First, a quote-unquote “recap.”

There are no limitations on how quotations can be styled. Block and pull quotes can both be eye-catching design elements, but they convey different messages. While a block quote is typically inside the content flow, a pull quote (sometimes called a callout) is extracted from the text to form a separate element.

Pull quotes extracted from the text

The proper HTML for marking up a block quote depends on its contents. My design for Patty Meltt includes concert reviews, which contain the reviewer’s name:

<blockquote> <p>"Patty Meltt’s powerful ballads and toe-tapping anthems had the audience singing along all night."</p> <footer>— Waylon Bootscuffer</footer> </blockquote>

Here, the footer contains information about the source or author of the parent element. This makes it a good fit for attributions inside a blockquote, where it indicates who wrote it. But what about cite?

For years, I used the cite element to mark up attributions. It’s one of those sneaky bits of HTML that felt intuitive until I read the spec and went, “Dagnabbit!” because cite isn’t meant to label people. Instead, it should be used for:

“The title of a creative work (e.g. book, website, song, painting, etc.)”

<blockquote> <p>"Patty Meltt’s powerful ballads and toe-tapping anthems had the audience singing along all night."</p> <footer>— Waylon Bootscuffer, <cite>Country Music Magazine</cite></footer> </blockquote>

So, in that example, footer marks up the attribution, and cite points to the title of the publication, not the person writing it. This gives the markup a semantic boost and helps people who use screen readers.

Styling with personality

Out-of-the-box, browsers do very little to style blockquotes, except for adding inline margins. You could add some simple blockquote styling, but with just a little more style, you can transform them into expressive design elements that reflect a brand’s personality and voice.

Quotation designs to reflect a brand’s personality and voice

For Patty Meltt’s design, I wanted her quotes to feel confident, loud, and a little over the top.

Tip: Interactive examples from this article are available in my lab.

Borders

A simple border, used well, can make block and pull quotes stand out and anchor them into a layout. A border on the left or top separates a block quote from surrounding content, helping a reader recognise it as a different voice from the main narrative.

In magazines and newspapers, block quotes punctuate content blocks and are frequently styled to contrast with the surrounding text. A full-width, bordered block quote encourages a reader to pause for a moment.

Block quote with left border (left) and a block quote with top border (right)

It’s a simple, yet effective, way to focus someone’s attention on a message. A thin border feels quiet and understated:

blockquote { padding-inline: 1.5rem; border-inline-start: 1px solid #98838e; border-inline-end: 1px solid #98838e; } Pull quotes with thin borders

This may suit some brands, but that’s not a style which reflects Patty’s personality. Whereas a bolder, thicker border feels more confident, like it has something important to say:

blockquote { padding-inline: 1.5rem; border-inline-start: 5px solid #98838e; border-inline-end: 5px solid #98838e; } Pull quotes with thick borders

Those borders needn’t always fill the full height or width of a blockquote, so instead of using the border property, use ::before and ::after pseudo-elements to add faux borders at any size:

blockquote { display: flex; flex-direction: column; align-items: center; } blockquote::before, blockquote::after { content: ""; display: block; width: 80px; height: 5px; background-color: #98838e; } Pull quote with faux borders

You could even animate those faux borders using keyframe animations or simple transitions to increase their width when someone interacts with the quotation:

blockquote::before, blockquote::after { content: ""; display: block; width: 80px; height: 5px; background-color: #98838e; transition: 300ms width ease-in-out; } blockquote:hover::before, blockquote:hover::after { width: 100%; } Quote marks

Before choosing how to style your quote marks, consider whether you need them at all. Technically, an HTML blockquote implies its content is a quotation. So, from an accessibility and semantic standpoint, quote marks aren’t required because screen readers and search engines will recognise a blockquote. However, quote marks can visually emphasise quoted content and add interest and personality to a design.

Quote marks add interest and personality

Are both opening and closing marks always needed? Possibly, when a design needs a traditional feel, or a quotation appears in a passage of running text:

blockquote { position: relative; padding-inline: 64px; } blockquote img:first-of-type, blockquote img:last-of-type { position: absolute; } blockquote img:first-of-type { top: 0; left: 0; } blockquote img:last-of-type { right: 0; bottom: 0; } Decorative oversized opening mark

Or, to give a design an editorial feel, you might use only a decorative oversized opening mark for a pull quote, which is separate from the normal flow of text:

blockquote { display: flex; flex-direction: column; align-items: center; } blockquote::after { content: ""; display: block; width: 80px; height: 5px; background-color: #98838e; } Quote marks library

Block quotes don’t necessarily need quote marks, but when you use them with purpose, they become more than punctuation. They become part of the design personality. Decorative marks are ideal when a brand wants to infuse its character into a design.

Poppins quote mark (left) and a Punch 3D quote mark (right)

Sadly, even the nicest designed typefaces can include dull and uninspiring quote marks. So, it’s important to remember that you can choose marks from an altogether different font if they better suit a design.

Part of my quote marks library

That’s why, whenever I audition a new typeface, I check its quote marks. If they’re memorable or noteworthy, I add them as SVGs to my quote marks library so I can easily find them later.

Shapes

Quotation design needn’t stop at borders and quote marks. Block and pull quotes can be any shape. You might style an album or concert review as a speech or thought bubble, and include an avatar for the author. Or, you could use a clip-path or mask to transform a quotation into any shape you can imagine.

Speech bubble, thought bubble, and blob Designing for Patty Meltt

Patty Meltt wanted a website packed with design details. Every element added to a design is an opportunity to be expressive, and that includes her quotations. From the selection of designs I showed her, she felt a mixture of quote marks, avatar images, and borders — with type set in a flowing script — best suited her style.

Design for Patty Meltt’s block quote (left) and pull quote (right)

To implement her pull quote, I used a cursive typeface, which contrasts with the rest of her typographic design:

blockquote p { font-family: "Lobster Two", cursive; font-size: 1.5rem; font-weight: 700; font-style: italic; text-transform: unset; }

Then I added an SVG quote mark from the Ohno type foundry’s Blazeface type family.

<div> <img src="img/blazeface-start.svg" alt="" width="48"> </div>

I turned its parent division into a flex container and aligned the contents vertically:

blockquote div { display: flex; align-items: center; gap: 1.5rem; }

…and used generated content to add a flexible-width horizontal line to fill any remaining space:

blockquote div:first-of-type::after { content: ""; display: block; flex: 1; height: 5px; background-color: #98838e; } Conclusion

With a little care and creativity, block quotes can become expressive, brand-building elements, as distinctive as a logo or headline. Whether you’re working with quiet, thoughtful quotes or loud, in-your-face testimonials, styling them is an opportunity to reinforce a client’s personality and voice.

Patty Meltt’s quotations became mini design statements. But the same principles apply no matter the brand: get the semantics right, choose styling that fits the tone, and don’t be afraid to experiment with borders, quote marks, and even shapes.

Getting Creative With Quotes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Stuff & Nonsense Practical Layout Workshop

Css Tricks - Mon, 08/11/2025 - 2:16am

We don’t publish a big ol’ bunch of links pushing products and whatnot around here. But I do like sharing a good resource when it’s available and that’s what I’m doing here with Andy Clarke’s upcoming Practical Layout Workshop.

First off, the deets:

  • Date: Thursday, 18th September
  • Time: 3:00 p.m. (UK)
  • Duration: 2 hours (live, with demos and Q&A)
  • Format: Online (join from anywhere), recording included
  • Price: £69

You probably already know Andy pretty well. He’s been a leading voice in web design forever. He’s also written a slew of super CSS-Tricks articles recently (including this one today, with more on the way!) that are all about “getting creative” with common UI patterns while leveraging CSS to embrace a healthy dose of whimsy.

Here’s what he had to say to me about the workshop:

Over the past year, I’ve been exploring how classic layout principles from comics, posters, and magazines can bring energy and storytelling to digital design. This session is where I share what I’ve learned, with examples, demos, and practical takeaways you can actually use.

Just get a whiff of the stuff he’s been digging into:

Article on Jun 3, 2025 Getting Creative With HTML Dialog Andy Clarke Article on Jul 18, 2025 Getting Creative With Versal Letters Andy Clarke Article on Jan 27, 2025 Revisiting CSS Multi-Column Layout Andy Clarke Article on Mar 21, 2025 Revisiting CSS border-image Andy Clarke Article on Apr 30, 2025 Revisiting Image Maps Andy Clarke

And when you pair that with the Toon Title demos he’s been publishing, you get a really good idea of the clever ideas and solid techniques you’ll take away from this workshop.

Oh, and CSS-Tricks readers can get in at a £5 discount with coupon code FRIEND5 (that’s the number 5 at the end) at checkout. &#x1f609;

Stuff & Nonsense Practical Layout Workshop originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

How to Prepare for CSS-Specific Interview Questions

Css Tricks - Fri, 08/08/2025 - 3:42am

If you landed on this article, chances are you might have a front-end interview coming up, perhaps one with a focus on CSS, or you are in the market to start preparing for CSS-related interviews altogether. Depending on the exact role you are interviewing for, it’s unlikely that you will only be asked questions about CSS. Typically, you will encounter a combination questions covering things like HTML, CSS, and JavaScript.

For this article, we will focus primarily on a set of 10 CSS questions you likely will encounter in front-end interviews, even if they get grouped with HTML or JavaScript. And to be clear, these may or may not be the “best” questions for an interviewer to ask, but what you are likely to see, based on my experience as the founder of frontendlead.com, an all-in-one platform to help front-end engineers prepare for big tech interviews. I have been a software engineer for over 13 years and have been on both ends of hundreds of interviews.

The questions cover different levels of difficulty. To keep things simple, we will start with the “easiest” questions and end with the “hardest” ones.

#Interview QuestionDifficulty1How would you go about building a responsive website?Easy2What are CSS preprocessors, and why are they useful?Easy3How would you make fonts responsive in CSS?Easy4Describe z-index and how stacking context is formed.Medium5What’s the difference between block, inline, and inline-block?Medium6What does * { box-sizing: border-box; } do?Medium7How would you go about making an image responsive in CSS?Medium8How would you make CSS more performant?Hard9What are the pros and cons of CSS in JS vs external CSS import, and which would you choose?Hard10Can you build this layout in CSS?Hard

Before we dive in, I’d like to say that there are many ways to correctly answer the same question. Everything I’m providing here is merely guidance for approaching the types of questions you may face in an interview. The actual questions you encounter may need more elaboration in a particular area or require specific examples that demonstrate your understanding of different concepts.

1. How would you go about building a responsive website? (Easy)

Responsive design is one of the fundamentals you’ll be asked about. Building a responsive website means your layout, images, and typography adapt gracefully to any device or screen size.

The basic tools for responsive design include relative units (such as %, em, and rem), media queries, and fluid layouts. Most interviews expect you to mention a “mobile-first” approach, where your base styles are designed for mobile devices and scaled up for larger screens.

A quick code example using media queries:

/* Main container for your page content, centered and with a max width for larger screens */ .container { max-width: 1200px; /* Prevents content from stretching too wide on large displays */ margin: 0 auto; /* Horizontally center the container */ padding: 16px; /* Adds space inside the container */ } /* Make all images scale with their parent container */ img { max-width: 100%; /* Image will never be wider than its container */ height: auto; /* Keeps the aspect ratio intact */ display: block; /* Removes extra space below images (inline images have baseline spacing) */ } /* Responsive styles for small screens (phones, small tablets) */ @media (max-width: 600px) { .container { padding: 8px; /* Reduce padding to save space on smaller screens */ } /* Example: Stack nav links vertically on small screens nav ul { flex-direction: column; } */ }

You should also mention how you handle navigation and images on mobile devices (such as collapsing navigational menus or leveraging responsive image techniques), as well as how to test layouts using browser Developer Tools.

2. What are CSS preprocessors, and why are they useful? (Easy)

CSS preprocessors, such as SassLess, and Stylus, make writing and maintaining large CSS codebases significantly easier. They add features that aren’t in vanilla CSS, such as mixins, and functions — although those lines are becoming more blurred as CSS ships similar features, such as variablesnesting, and yes, mixins and functions.

Mixins and functions enable you to reuse common patterns and even generate code based on parameters. Here’s an example in Sass:

// Mixin: For a common box shadow you want to reuse @mixin shadow($opacity: 0.12) { box-shadow: 0 2px 8px 0 rgba(24, 39, 75, $opacity); } // Function: Calculate a spacing value for consistent margins and padding @function space($multiplier: 1) { @return $multiplier * 8px; } // Placeholder selector: For base button styles to extend %btn-base { display: inline-block; font-size: $font-size-lg; border-radius: 6px; text-align: center; cursor: pointer; } // Partial import: Example (would be in _variables.scss) // @import 'variables'; // Button styles using everything above .button { @extend %btn-base; // Use base button styles background: $primary; color: #fff; padding: space(1.5) space(3); // Use the custom function for spacing @include shadow(0.15); // Use the mixin for shadow // Nested selector for hover state &:hover { background: lighten($primary, 10%); } // Modifier class (e.g., .button.secondary) &.secondary { background: $secondary; color: #23272f; border: 2px solid $secondary; } // Nested media query (for responsive buttons) @media (max-width: 600px) { padding: space(1) space(2); font-size: 1rem; } }

Preprocessors help keep your codebase DRY (Don’t Repeat Yourself) and make refactoring less painful. While CSS now has native variables (--variable), preprocessors are still widely used for their advanced features.

This is a good opportunity to demonstrate your understanding of modern CSS as well since CSS now supports nesting and work on functions is underway (and indeed are already planned for Chrome 139).

3. How would you make fonts responsive in CSS? (Easy)

Font sizing is a common interview topic because it affects both design and accessibility. Responsive fonts adjust to screen size, ensuring your text remains readable. The classic approach is to use relative units, such as em (scoped to the parent element) and rem (scoped to the root element). Newer CSS features makes this even easier and more flexible with the clamp() function and viewport units (vw and vh). You can also use media queries to step up font sizes for larger screens.

Here are some practical examples:

/* Basic responsive text using rem (scales with root html font size) */ body { font-size: 1rem; /* 1rem is typically 16px, but can be increased for accessibility */ } /* Use rem for headings so they scale with user/browser settings */ h1 { font-size: 2.5rem; /* 2.5 × root font size */ line-height: 1.2; } /* Modern fluid sizing with clamp and viewport units */ h2 { /* Font size is at least 1.5rem, scales with viewport up to 3rem */ font-size: clamp(1.5rem, 4vw, 3rem); } /* Using viewport width units directly */ h3 { font-size: 6vw; /* 6% of viewport width (can get very large/small on extremes) */ } /* Responsive font-size using media queries (manual step-up) */ p { font-size: 1rem; } @media (min-width: 600px) { p { font-size: 1.2rem; } } @media (min-width: 1200px) { p { font-size: 1.4rem; } }
  • rem/em units make your text scale with the root or parent font size, making them more responsive to changes.
  • clamp() lets you set a minimum, fluid, and maximum font size at once (e.g., clamp(1.5rem, 4vw, 3rem) ensures the font size never falls below 1.5rem or exceeds 3rem, scaling smoothly in between).
  • Viewport units (vw, vh) make fonts fluid relative to the screen width or height.
  • Media queries enable fine-tuning of font size for various devices and breakpoints.

Absolute px units are usually avoided for body text, as they don’t scale for users who adjust browser settings for accessibility. Speaking of accessibility, it’s worth calling out that extra consideration needs to go into the possibility of the user zooming into the page.

4. Describe the z-index property and how stacking context is formed. (Medium)

Thez-index property determines which elements appear on top of others, but it only works on elements that have a positioning context, such as position: relative, absolute, or fixed.

stacking context is an environment where stacking and rendering order is controlled. New stacking contexts can be created by elements with specific properties, such as position with a z-index, or CSS properties like opacity less than 1, transform, or filter.

Understanding stacking context is essential for UI components like drop-downs, modals, and tooltips.

Here’s an example demonstrating a stacking context created by a parent element element that contains two children that stack one on top of the other, ordered by z-index:

/* The parent creates a new stacking context by having position and z-index */ .parent { position: relative; /* Triggers a positioning context */ z-index: 2; /* This parent will stack above siblings with lower z-index values */ width: 300px; height: 200px; background: #b3e6fc; margin: 32px; } /* The child is absolutely positioned inside .parent */ .child { position: absolute; /* Needed for z-index to work */ top: 40px; left: 40px; width: 200px; height: 100px; background: #4f46e5; color: #fff; z-index: 10; /* Relative to its parent's stacking context, not the whole page */ display: flex; align-items: center; justify-content: center; } /* Another sibling element at the root level for comparison */ .sibling { position: relative; z-index: 1; /* Lower than .parent, so .parent stacks on top */ width: 320px; height: 140px; background: #fca311; margin: -80px 0 0 220px; /* Overlap with .parent for demo */ display: flex; align-items: center; justify-content: center; color: #23272f; }

If you have ever run into an issue where z-index isn’t behaving as you expect, check if there’s an unexpected stacking context due to a parent element.

5. What’s the difference between the display property’s block, inline, and inline-block values? (Medium)

When you’re asked about the difference between the display property’s block, inline, and inline-block values in CSS, remember that they determine how elements are displayed in the document flow.

  • Block elements always start on a new line and take up the full width of their parent container, regardless of their actual content. Examples include <div> and <p>.
  • Inline elements flow within a line of text, only occupying as much width as needed for their content; you cannot set their width or height. Examples include <span> and <a>.
  • Inline-block elements combine the behaviors of both inline and block elements: They flow inline with text (without forcing a new line), but you can set their width and height like a block element, which makes them especially useful for custom buttons or navigation items.
Display ValueStarts New Line?Width/Height Settable?Example ElementsblockYesYes<div>, <p>, <h1>inlineNoNo<span>, <a>, <strong>inline-blockNoYesCustom buttons, images, icons 6. What does box-sizing: border-box do? (Medium)

By default, CSS uses the content-box model, which means that width and height only apply to the content, excluding padding and border. box-sizing: border-box changes this so that width and height include the padding and border, making sizing more predictable.

Here’s an example of how that might be demonstrated in CSS:

/* Apply border-box sizing to all elements and their pseudo-elements */ *, *::before, *::after { box-sizing: border-box; /* Width and height now include padding and border */ } /* Demo: Without border-box (the default, content-box) */ .box-content { box-sizing: content-box; width: 200px; padding: 20px; border: 4px solid #2563eb; background: #f0f4ff; margin-bottom: 16px; /* The real rendered width will be: 200px (content) + 40px (padding) + 8px (border) = 248px */ } /* Demo: With border-box */ .box-border { box-sizing: border-box; width: 200px; padding: 20px; border: 4px solid #16a34a; background: #e7faed; /* The rendered width will be exactly 200px, since padding and border are included in the width */ }

With border-box, you avoid the classic issue where adding padding or a border makes your boxes overflow their parent or break your layout. It’s now a standard best practice. You can even say that Chris Coyier has deemed February 1 “International box-sizing Awareness Day” which totally should be a real thing.

7. How would you go about making an image responsive in CSS? (Medium)

This is a deceptively hard question because responsive images is a topic big enough for an entire guide. The classic approach is to ensure that photos never exceed the width of their container. For most cases, that means setting a max-width on the image element and ensuring it maintains its proportions:

/* 1. Make images responsive to their container width */ img { max-width: 100%; /* Prevents the image from overflowing its parent */ height: auto; /* Maintains aspect ratio */ display: block; /* Removes bottom whitespace that inline images have */ }

For images that need to maintain a specific aspect ratio (like a 16:9 video thumbnail), you can use the padding-bottom trick:

/* 2. Maintain a specific aspect ratio (e.g., 16:9) using the padding-bottom trick */ .responsive-img-container { position: relative; /* Needed for absolutely positioning the img */ width: 100%; /* Full width of the parent container */ padding-bottom: 56.25%; /* 16:9 aspect ratio (9/16 = 0.5625) */ overflow: hidden; /* Ensures image doesn’t overflow container */ } .responsive-img-container img { position: absolute; /* Take the image out of the normal flow */ top: 0; left: 0; width: 100%; /* Stretch to fill container */ height: 100%; /* Stretch to fill container */ object-fit: cover; /* Ensure the image covers the area, cropping if needed */ }

Modern CSS also has the aspect-ratio property for this:

/* 3. Use the aspect-ratio property for a cleaner approach (modern browsers) */ .aspect-ratio-img { aspect-ratio: 16 / 9; /* Maintain 16:9 ratio automatically */ width: 100%; height: auto; display: block; }

Responsive images often use the HTML srcset attribute and the <picture> element as well for high-DPI and various screen sizes. There’s an entire CSS-Tricks guide on those features. And, of course, there are performance considerations to take into account because the goal is to serve the best image format and size to the right device.

8. How would you make CSS more performant? (Hard)

CSS performance is crucial for delivering fast and smooth experiences, especially on large-scale websites or applications. Poor CSS practices can slow down page loads, increase rendering times, and make maintenance harder. There are several strategies you can use to keep your CSS efficient and your site responsive.

At the same time, CSS is often not the source of performance bottlenecks. It certainly contributes to it, but performance is a more nuanced field where many factors most certainly influence performance than CSS.

1. Minimize your bundle size

Large CSS files slow down initial page loads. Removing unused CSS (also called “dead code elimination”) can significantly reduce file size. Tools like PurgeCSSUnCSS, or the built-in features of frameworks like Next.js and Tailwind can scan your HTML/JSX and only keep styles that are used.

2. Split and lazy-load CSS

Instead of shipping all CSS at once, split your styles by page or feature (“code splitting”). Modern bundlers (such as webpack and Vite) and frameworks (like React, Vue, and Next.js) support the dynamic import() feature, allowing only the CSS required for the current route or component to be loaded.

// Dynamically load styles when the page loads import("./styles/page.css");

This technique improves “first paint” times and reduces bandwidth, especially for users who never visit certain pages.

3. Use simple, shallow selectors

Browsers read CSS from right to left and evaluate deeply nested or complex selectors more slowly. For best performance, use flat selectors like .btn instead of something like .header nav ul li a.active.

4. Minify and compress CSS

Before deploying, always minify your CSS using tools like cssnano or clean-css. Gzip or Brotli compression (handled by your server or CDN) will further shrink the payload sent to users.

5. Use critical CSS (or not!)

Critical CSS refers to inlining the minimal CSS required for above-the-fold content in the initial HTML. This allows the browser to render the visible part of the page immediately, while loading the rest of the CSS asynchronously.

I’d say this is a nice-to-have sort of thing, as it is a fragile and difficult strategy to implement and maintain.

6. Reduce the use of expensive properties

Specific CSS properties, such as heavy box shadows, filters, or animations on significant elements, can cause “repaints” and slow down rendering. Use these effects thoughtfully, and prefer transform and opacity for animating elements — the browser’s compositor can often optimize these.

7. Avoid !important and overly specific selectors

Frequent use of !important and particular selectors can make your CSS hard to override and debug, leading to more duplicated or conflicting rules.

8. Optimize unused CSS

Let’s face it. As a site is iterated, CSS often becomes larger, not smaller. Styles that were relevant at one point are superseded by new ones without fully replacing the older styles, often for fear of introducing unexpected changes in unknown places.

We have lots and lots of tools for detecting and removing unused CSS. There are limitations and possible trade-offs, of course, so your mileage may vary.

There there’s the case of UI kits or component libraries that import numerous unused styles. It’s easy (and maybe even tempting) to use all of the styles provided by a framework, but try importing only what you need, or use tree-shaking to strip unused parts. Many frameworks allow you to configure exactly what you need, like Bootstrap does.

10. Audit CSS regularly

Modern browser DevTools (like Chrome’s Coverage tab, Performance panel, and Rendering panel) let you see which styles are used on a page, helping you identify and remove dead code.

There are online tools as well, like the Specificity VisualizerCSS Specificity Graph Generator, and CSS Stats. You can find more information on these and more in “Tools for Auditing CSS”.

9. What are the pros and cons of CSS-in-JS vs. external CSS imports, and which would you choose? (Hard)

CSS-in-JS may not be the hot topic it was a few years go, but you’re still very likely to see it pop up in an interview. It’s not so much your duty to rail for or against it, but demonstrate your understanding of the concept and how it compares to external CSS imports.

Here’s how I would break it out.

CSS-in-JS (like styled-components, Emotion, or Stitches) ProsConsStyles are scoped to components, preventing unwanted side effects.Adds runtime overhead and may increase JS bundle size.Dynamic styling based on component state or props.Styles may not appear immediately on server-rendered pages without extra setup.Easy to maintain styles close to your component logic.It can be harder to debug in the browser inspector. External CSS imports (classic .css files, global or CSS Modules): ProsConsCSS is loaded by the browser in parallel, allowing for faster rendering.Risk of style collision in global CSS.Easier to cache and split CSS for large projects.Less dynamic—harder to do conditional styles based on state.Great for global themes, resets, or utility classes.

In practice, most modern teams use a combination of global styles and resets in CSS files, along with component-level styles using CSS-in-JS or CSS Modules.

10. Can you build this layout in CSS? (Hard)

You’ll almost always be asked to build layouts on the fly.

Remember, a question like this is a great opportunity because there’s more than one way to solve it. In this case, we’re looking at a pretty classic “Holy Grail” layout, something Geoff has written about before and demonstrated various ways to go about it using CSS Grid.

You could go with a Flexbox approach as well:

CodePen Embed Fallback

It would be easy to fall into the trap of finding the “best” solution, but this perhaps is one case where demonstrating how to think like a front-end web developer is equally, if not more, important than coming up with a single definitive solution.

Conclusion

These are merely example of the sort of core CSS questions you’re likely to encounter in front-end interviews, along with practical examples and the reasoning behind each approach. If you’re comfortable answering these in depth and can code out the examples under time pressure, you’ll be well-prepared.

For more front-end interview questions, consider exploring frontendlead.com, which helps you prepare for front-end interviews across top tech companies. If you have additional topics you’d like to see covered or encounter tricky interview questions, please feel free to post them in the comments — I’d love to see them.

And, of course, best of luck in your interviews!

How to Prepare for CSS-Specific Interview Questions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Bringing Back Parallax With Scroll-Driven CSS Animations

Css Tricks - Wed, 08/06/2025 - 3:39am

For a period in the 2010s, parallax was a guaranteed way to make your website “cool”. Indeed, Chris Coyier was writing about it as far back as 2008.

For those unfamiliar with the concept, parallax is a pattern in which different elements of a webpage move at varying speeds as the user scrolls, creating a three-dimensional, layered appearance. A true parallax effect was once only achievable using JavaScript. However, scroll-driven animations have now given us a CSS-only solution, which is free from the main-thread blocking that can plague JavaScript animations.

Parallax may have become a little cliché, but I think it’s worth revisiting with this new CSS feature.

Note: Scroll-driven animations are available on Chrome, Edge, Opera, and Firefox (behind a feature flag) at the time of writing. Use a supported browser when following this tutorial.

Starting code

In this example, we will apply parallax animations to the background and icons within the three “hero” sections of a universe-themed webpage. We’ll start with some lightly styled markup featuring alternating hero and text sections while including some space-related nonsense as placeholder content.

CodePen Embed Fallback Adding initial animations

Let’s add an animation to the background pattern within each hero section to modify the background position.

@keyframes parallax { from { background-position: bottom 0px center; } to { background-position: bottom -400px center; } } section.hero { /* previous code */ + animation: parallax 3s linear; }

Here we use the keyframes CSS rule to create a start and end position for the background. Then we attach this animation to each of our hero sections using the animation property.

By default, CSS animations are duration-based and run when the specified selector is loaded in the DOM. If you refresh your browser, you will see the animation running for three seconds as soon as the page loads.

We do not want our animation to be triggered immediately. Instead, we intend to use the page’s scroll position as a reference to calculate the animation’s progress.

Scroll-driven animations provide two new animation timeline CSS functions. These additions, view() and scroll(), tell the browser what to reference when calculating the progress of a CSS animation. We will use the view() function later, but for now, let’s focus on scroll(). The scroll progress timeline couples the progression of an animation to the user’s scroll position within a scroll container. Parameters can be included to change the scroll axis and container element, but these are not necessary for our implementation.

Let’s use a scroll progress timeline for our animation:

section.hero { /* previous code */ - animation: parallax 3s linear; + animation: parallax linear; + animation-timeline: scroll(); }

If you refresh the page, you will notice that as you scroll down, the position of the background of each hero section also changes. If you scroll back up, the animation reverses. As a bonus, this CSS animation is handled off the main thread and thus is not subject to blocking by any JavaScript that may be running.

Using the view progress timeline

Now let’s add a new parallax layer by animating the header text and icons within each hero section. This way, the background patterns, headers, and main page content will all appear to scroll at different speeds. We will initially use the scroll() CSS function for the animation timeline here as well.

@keyframes float { from { top: 25%; } to { top: 50%; } } .hero-content { /* previous code */ + position: absolute; + top: 25%; + animation: float linear; + animation-timeline: scroll(); }

That’s not quite right. The animation for the sections further down the page is nearly done by the time they come into view. Luckily, the view animation timeline solves this problem. By setting the animation-timeline property to view(), our animation progresses based on the position of the subject within the scrollport, which is the part of the container that is visible when scrolling. Like the scroll animation timeline, scrolling in reverse will also reverse the animation.

Let’s try changing our animation timeline property for the hero text:

.hero-content { /* previous code */ - animation-timeline: scroll(); + animation-timeline: view(); }

That looks pretty good, but there is a problem with the header content flashing into the view when scrolling back up the document. This is because the view timeline is calculated based on the original, pre-animation positioning of the subject element.

We can solve this by adding an inset parameter to the view() function. This adjusts the size of the container in which the animation will take place. According to MDN’s documentation, the “inset is used to determine whether the element is in view which determines the length of the animation timeline. In other words, the animation lasts as long as the element is in the inset-adjusted view.”

So, by using a negative value, we make the container larger than the window and trigger the animation to start a little before and end a little after the subject is visible. This accounts for the fact that the subject moves during the animation.

- animation-timeline: view(); + animation-timeline: view(-100px);

Now both the text and background animate smoothly at different speeds.

CodePen Embed Fallback Adjusting animations using animation ranges

So far, we have employed both scroll and view progress timelines. Let’s look at another way to adjust the start and end timing of the animations using the animation-range property. It can be used to modify where along the timeline the animation will start and end.

We’ll start by adding a view() timeline animation to the #spaceship emoji:

@keyframes launch { from { transform: translate(-100px, 200px); } to { transform: translate(100px, -100px); } } #spaceship { animation: launch; animation-timeline: view(); }

Again, we see the emoji returning to its 0% position once its original unanimated position is outside of the scrollport.

As discussed before, animations are based on the original pre-animation position of the subject. Previously, we solved this by adding an inset parameter to the view() function. We can also adjust the animation range and tell our animation to continue beyond 100% of the animation timeline without having to manipulate the inset of the view timeline itself.

#spaceship { animation: launch; animation-timeline: view(); + animation-range: 0% 120%; }

Now the animation continues until we have scrolled an extra 20% beyond the calculated scroll timeline’s normal endpoint.

Let’s say that we want to add an animation to the #comet emoji, but we don’t want it to start animating until it has passed 4rem from the bottom of the scrollport:

@keyframes rotate { from { transform: rotate(0deg) translateX(100px); } to { transform: rotate(-70deg) translateX(0px); } } #comet { animation: rotate linear; transform-origin: center 125px; animation-timeline: view(); animation-range: 4rem 120%; }

Here we see the “delayed” animation in action:

We can also combine animation ranges to run completely different animations at different points within the same timeline! Let’s illustrate this by combining animation ranges for the #satellite icon at the top of the page. The result is that the first animation runs until the icon passes 80% of the scrollport, then the second animation takes over for the final 20%.

@keyframes orbit-in { 0% { transform: rotate(200deg); } 100% { transform: rotate(0deg); } } @keyframes orbit-out { 0% { transform: translate(0px, 0px); } 100% { transform: translate(-50px, -15px); } } #satellite { animation: orbit-in linear, orbit-out ease; animation-timeline: view(); animation-range: 0% 80%, 80% 110%; } Fallbacks and accessibility

Our webpage features numerous moving elements that may cause discomfort for some users. Let’s consider accessibility for motion sensitivities and incorporate the prefers-reduced-motion CSS media feature.

There are two possible values: no-preference, and reduce. If we want to fine-tune the webpage with animations disabled by default and then enhance each selector with animations and associated styles, then we can use no-preference to enable them.

@media (prefers-reduced-motion: no-preference) { .my-selector { position: relative; top: 25%; animation: cool-animation linear; animation-timeline: scroll(); } }

For us, however, the webpage content and images will still all be visible if we disable all animations simultaneously. This can be done concisely using the reduce option. It’s important to note that this sort of blanket approach works for our situation, but you should always consider the impact on your specific users when implementing accessibility features.

@media (prefers-reduced-motion: reduce) { .my-selector { animation: none !important; } }

In addition to considering accessibility, we should also take into account that scroll-driven animations are not supported by all browsers at the time of writing. If we care a lot about users seeing our animations, we can add a polyfill (direct link) to extend this functionality to currently unsupported browsers. This, however, will force the animation to run on the main thread.

Alternatively, we could decide that performance is important enough to skip the animations on unsupported browsers, thereby keeping the main thread clear. In this case, we can use the @supports selector and include the styles only on supported browsers.

Here is the final code with everything, including the polyfill and reduced motion fallback:

CodePen Embed Fallback Conclusion

There we go, we just re-created a classic web effect with scroll-driven animations using scroll and view progress timelines. We also discussed some of the parameters that can be used to adjust animation behavior. Whether or not parallax is your thing, I like the idea that we can use a modern approach that is capable of doing what we could before… only better with a dash of progressive enhancement.

More information

Bringing Back Parallax With Scroll-Driven CSS Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Keeping Article Demos Alive When Third-Party APIs Die

Css Tricks - Wed, 07/30/2025 - 3:21am

After four years, the demos in my “Headless Form Submission with the WordPress REST API” article finally stopped working.

The article includes CodePen embeds that demonstrate how to use the REST API endpoints of popular WordPress form plugins to capture and display validation errors and submission feedback when building a completely custom front-end. The pens relied on a WordPress site I had running in the background. But during a forced infrastructure migration, the site failed to transfer properly, and, even worse, I lost access to my account.

Sure, I could have contacted support or restored a backup elsewhere. But the situation made me wonder: what if this had not been WordPress? What if it were a third-party service I couldn’t self-host or fix? Is there a way to build demos that do not break when the services they rely on fail? How can we ensure educational demos stay available for as long as possible?

Or is this just inevitable? Are demos, like everything else on the web, doomed to break eventually?

Parallels with software testing

Those who write tests for their code have long wrestled with similar questions, though framed differently. At the core, the issue is the same. Dependencies, especially third-party ones, become hurdles because they are outside the bounds of control.

Not surprisingly, the most reliable way to eliminate issues stemming from external dependencies is to remove the external service entirely from the equation, effectively decoupling from it. Of course, how this is done, and whether it’s always possible, depends on the context.

As it happens, techniques for handling dependencies can be just as useful when it comes to making demos more resilient.

To keep things concrete, I’ll be using the mentioned CodePen demos as an example. But the same approach works just as well in many other contexts.

Decoupling REST API dependencies

While there are many strategies and tricks, the two most common approaches to breaking reliance on a REST API are:

  1. Mocking the HTTP calls in code and, instead of performing real network requests, returning stubbed responses
  2. Using a mock API server as a stand-in for the real service and serving predefined responses in a similar manner

Both have trade-offs, but let’s look at those later.

Mocking a response with an interceptor

Modern testing frameworks, whether for unit or end-to-end testing, such as Jest or Playwright, offer built-in mocking capabilities.

However, we don’t necessarily need these, and we can’t use them in the pens anyway. Instead, we can monkey patch the Fetch API to intercept requests and return mock responses. With monkey patching, when changing the original source code isn’t feasible, we can introduce new behavior by overwriting existing functions.

Implementing it looks like this:

const fetchWPFormsRestApiInterceptor = (fetch) => async ( resource, options = {} ) => { // To make sure we are dealing with the data we expect if (typeof resource !== "string" || !(options.body instanceof FormData)) { return fetch(resource, options); } if (resource.match(/wp-json\/contact-form-7/)) { return contactForm7Response(options.body); } if (resource.match(/wp-json\/gf/)) { return gravityFormsResponse(options.body); } return fetch(resource, options); }; window.fetch = fetchWPFormsRestApiInterceptor(window.fetch);

We override the default fetch with our own version that adds custom logic for specific conditions, and otherwise lets requests pass through unchanged.

The replacement function, fetchWPFormsRestApiInterceptor, acts like an interceptor. An interceptor is simply a pattern that modifies requests or responses based on certain conditions.

Many HTTP libraries, like the once-popular axios, offer a convenient API to add interceptors without resorting to monkey patching, which should be used sparingly. It’s all too easy to introduce subtle bugs unintentionally or create conflicts when managing multiple overrides.

With the interceptor in place, returning a fake response is as simple as calling the static JSON method of the Response object:

const contactForm7Response = (formData) => { const body = {} return Response.json(body); };

Depending on the need, the response can be anything from plain text to a Blob or ArrayBuffer. It’s also possible to specify custom status codes and include additional headers.

For the CodePen demo, the response might be built like this:

const contactForm7Response = (formData) => { const submissionSuccess = { into: "#", status: "mail_sent", message: "Thank you for your message. It has been sent.!", posted_data_hash: "d52f9f9de995287195409fe6dcde0c50" }; const submissionValidationFailed = { into: "#", status: "validation_failed", message: "One or more fields have an error. Please check and try again.", posted_data_hash: "", invalid_fields: [] }; if (!formData.get("somebodys-name")) { submissionValidationFailed.invalid_fields.push({ into: "span.wpcf7-form-control-wrap.somebodys-name", message: "This field is required.", idref: null, error_id: "-ve-somebodys-name" }); } // Or a more thorough way to check the validity of an email address if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.get("any-email"))) { submissionValidationFailed.invalid_fields.push({ into: "span.wpcf7-form-control-wrap.any-email", message: "The email address entered is invalid.", idref: null, error_id: "-ve-any-email" }); } // The rest of the validations... const body = !submissionValidationFailed.invalid_fields.length ? submissionSuccess : submissionValidationFailed; return Response.json(body); };

At this point, any fetch call to a URL matching wp-json/contact-form-7 returns the faked success or validation errors, depending on the form input.

Now let’s contrast that with the mocked API server approach.

Mocked API server with serverless

Running a traditionally hosted mock API server reintroduces concerns around availability, maintenance, and cost. Even though the hype around serverless functions has quieted, we can sidestep these issues by using them.

And with DigitalOcean Functions offering a generous free tier, creating mocked APIs is practically free and requires no more effort than manually mocking them.

For simple use cases, everything can be done through the Functions control panel, including writing the code in the built-in editor. Check out this concise presentation video to see it in action:

For more complex needs, functions can be developed locally and deployed using doctl (DigitalOcean’s CLI).

To return the mocked response, it’s easier if we create a separate Function for each endpoint, since we can avoid adding unnecessary conditions. Fortunately, we can stick with JavaScript (Node.js), and starting with nearly the same base we used for contactForm7Response:

function main(event) { const body = {}; return { body }; }

We must name the handler function main, which is invoked when the endpoint is called. The function receives the event object as its first argument, containing the details of the request. Once again, we could return anything, but to return the JSON response we need, it’s enough to simply return an object.

We can reuse the same code for creating the response as-is. The only difference is that we have to extract the form input data from the event as FormData ourselves:

function main(event) { // How do we get the FormData from the event? const formData = new FormData(); const submissionSuccess = { // ... }; const submissionValidationFailed = { // ... }; if (!formData.get("somebodys-name")) { submissionValidationFailed.invalid_fields.push({ // ... }); } // Or a more thorough way to check the validity of an email address if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.get("any-email"))) { submissionValidationFailed.invalid_fields.push({ // ... }); } // The rest of the validations... const body = !submissionValidationFailed.invalid_fields.length ? submissionSuccess : submissionValidationFailed; return { body }; }

As far as converting the data, serverless functions typically expect JSON inputs, so for other data types an extra parsing step is required. As it happens, the forms in the CodePen demos are submitted as multipart/form-data.

Without any libraries, we can convert a multipart/form-data string into a FormData by taking advantage of the Response API’s capabilities:

async function convertMultipartFormDataToFormData(data) { const matches = data.match(/^\s*--(\S+)/); if (!matches) { return new FormData(); } const boundary = matches[1]; return new Response(data, { headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` } }).formData(); }

The code is mostly focused on extracting the boundary variable. This is typically autogenerated, for example, when submitting a form in a browser.

The submitted raw data is available via event.http.body, but since it’s base64-encoded, we need to decode it first:

async function main(event) { const formData = await convertMultipartFormDataToFormData( Buffer.from(event?.http?.body ?? "", "base64").toString("utf8") ); // ... const body = !submissionValidationFailed.invalid_fields.length ? submissionSuccess : submissionValidationFailed; return { body }; }

And that’s it. With this approach, all that’s left is to replace calls to the original APIs with calls to the mocked ones.

Closing thoughts

Ultimately, both approaches help decouple the demos from the third-party API dependency. In terms of effort, at least for this specific example, they seem comparable.

It’s hard to beat the fact that there’s no external dependency with the manual mocking approach, not even on something we somewhat control, and everything is bundled together. In general, without knowing specific details, there are good reasons to favor this approach for small, self-contained demos.

But using a mocked server API also has its advantages. A mocked server API can power not only demos, but also various types of tests. For more complex needs, a dedicated team working on the mocked server might prefer a different programming language than JavaScript, or they might opt to use a tool like WireMock instead of starting from scratch.

As with everything, it depends. There are many criteria to consider beyond what I’ve just mentioned.

I also don’t think this approach necessarily needs to be applied by default. After all, I had the CodePen demos working for four years without any issues.

The important part is having a way to know when demos break (monitoring), and when they do, having the right tools at our disposal to handle the situation.

Keeping Article Demos Alive When Third-Party APIs Die originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Making a Masonry Layout That Works Today

Css Tricks - Mon, 07/28/2025 - 2:42am

Many CSS experts have weighed heavily on possible syntaxes for a new masonry layout feature last year. There were two main camps and a third camp that strikes a balance between the two:

  1. Use display: masonry
  2. Use grid-template-rows: masonry
  3. Use item-pack: collapse

I don’t think they’ve came up with a resolution yet. But you might want to know that Firefox already supports the masonry layout with the second syntax. And Chrome is testing it with the first syntax. While it’s cool to see native support for CSS Masonry evolving, we can’t really use it in production if other browsers don’t support the same implementation…

So, instead of adding my voice to one of those camps, I went on to figure out how make masonry work today with other browsers. I’m happy to report I’ve found a way — and, bonus! — that support can be provided with only 66 lines of JavaScript.

In this article, I’m gonna show you how it works. But first, here’s a demo for you to play with, just to prove that I’m not spewing nonsense. Note that there’s gonna be a slight delay since we’re waiting for an image to load first. If you’re placing a masonry at the top fold, consider skipping including images because of this!

Anyway, here’s the demo:

CodePen Embed Fallback What in the magic is this?!

Now, there are a ton of things I’ve included in this demo, even though there are only 66 lines of JavaScript:

  • You can define the masonry with any number of columns.
  • Each item can span multiple columns.
  • We wait for media to load before calculating the size of each item.
  • We made it responsive by listening to changes with the ResizeObserver.

These make my implementation incredibly robust and ready for production use, while also way more flexible than many Flexbox masonry knockoffs out there on the interwebs.

Now, a hot tip. If you combine this with Tailwind’s responsive variants and arbitrary values, you can include even more flexibility into this masonry grid without writing more CSS.

Okay, before you get hyped up any further, let’s come back to the main question: How the heck does this work?

Let’s start with a polyfill

Firefox already supports masonry layouts via the second camp’s syntax. Here’s the CSS you need to create a CSS masonry grid layout in Firefox.

.masonry { display: grid; grid-template-columns: repeat( auto-fit, minmax(min(var(--item-width, 200px), 100%), 1fr) ); grid-template-rows: masonry; grid-auto-flow: dense; /* Optional, but recommended */ }

Since Firefox already has native masonry support, naturally we shouldn’t mess around with it. The best way to check if masonry is supported by default is to check if grid-template-rows can hold the masonry value.

function isMasonrySupported(container) { return getComputedStyle(container).gridTemplateRows === 'masonry' }

If masonry is supported, we’ll skip our implementation. Otherwise, we’ll do something about it.

const containers = document.querySelectorAll('.masonry') containers.forEach(async container => { if (isMasonrySupported(container)) return }) Masonry layout made simple

Now, I want to preface this segment that I’m not the one who invented this technique.

I figured out this technique when I was digging through the web, searching for possible ways to implement a masonry grid today. So kudos goes to the unknown developer who developed the idea first — and perhaps me for understanding, converting, and using it.

The technique goes like this:

  1. We set grid-auto-rows to 0px.
  2. Then we set row-gap to 1px.
  3. Then we get the item’s height through getBoundingClientRect.
  4. We then size the item’s “row allocation” by adding the height the column-gap value together.

This is really unintuitive if you’ve been using CSS Grid the standard way. But once you get this, you can also grasp how this works!

Now, because this is so unintuitive, we’re gonna take things step-by-step so you see how this whole thing evolves into the final output.

Step by step

First, we set grid-auto-rows to 0px. This is whacky because every grid item will effectively have “zero height”. Yet, at the same time, CSS Grid maintains the order of the columns and rows!

containers.forEach(async container => { // ... container.style.gridAutoRows = '0px' })

Second, we set row-gap to 1px. Once we do this, you begin to notice an initial stacking of the rows, each one one pixel below the previous one.

containers.forEach(async container => { // ... container.style.gridAutoRows = '0px' container.style.setProperty('row-gap', '1px', 'important') })

Third, assuming there are no images or other media elements in the grid items, we can easily get the height of each grid item with getBoundingClientRect.

We can then restore the “height” of the grid item in CSS Grid by substituting grow-row-end with the height value. This works because each row-gap is now 1px tall.

When we do this, you can see the grid beginning to take shape. Each item is now (kinda) back at their respective positions:

containers.forEach(async container => { // ... let items = container.children layout({ items }) }) function layout({ items }) { items.forEach(item => { const ib = item.getBoundingClientRect() item.style.gridRowEnd = `span ${Math.round(ib.height)}` }) }

We now need to restore the row gap between items. Thankfully, since masonry grids usually have the same column-gap and row-gap values, we can grab the desired row gap by reading column-gap values.

Once we do that, we add it to grid-row-end to expand the number of rows (the “height”) taken up by the item in the grid:

containers.forEach(async container => { // ... const items = container.children const colGap = parseFloat(getComputedStyle(container).columnGap) layout({ items, colGap }) }) function layout({ items, colGap }) { items.forEach(item => { const ib = item.getBoundingClientRect() item.style.gridRowEnd = `span ${Math.round(ib.height + colGap)}` }) }

And, just like that, we’ve made the masonry grid! Everything from here on is simply to make this ready for production.

Waiting for media to load

Try adding an image to any grid item and you’ll notice that the grid breaks. That’s because the item’s height will be “wrong”.

It’s wrong because we took the height value before the image was properly loaded. The DOM doesn’t know the dimensions of the image yet. To fix this, we need to wait for the media to load before running the layout function.

We can do this with the following code (which I shall not explain since this is not much of a CSS trick &#x1f605;):

containers.forEach(async container => { // ... try { await Promise.all([areImagesLoaded(container), areVideosLoaded(container)]) } catch(e) {} // Run the layout function after images are loaded layout({ items, colGap }) }) // Checks if images are loaded async function areImagesLoaded(container) { const images = Array.from(container.querySelectorAll('img')) const promises = images.map(img => { return new Promise((resolve, reject) => { if (img.complete) return resolve() img.onload = resolve img.onerror = reject }) }) return Promise.all(promises) } // Checks if videos are loaded function areVideosLoaded(container) { const videos = Array.from(container.querySelectorAll('video')) const promises = videos.map(video => { return new Promise((resolve, reject) => { if (video.readyState === 4) return resolve() video.onloadedmetadata = resolve video.onerror = reject }) }) return Promise.all(promises) }

Voilà, we have a CSS masnory grid that works with images and videos!

Making it responsive

This is a simple step. We only need to use the ResizeObserver API to listen for any change in dimensions of the masonry grid container.

When there’s a change, we run the layout function again:

containers.forEach(async container => { // ... const observer = new ResizeObserver(observerFn) observer.observe(container) function observerFn(entries) { for (const entry of entries) { layout({colGap, items}) } } })

This demo uses the standard Resize Observer API. But you can make it simpler by using the refined resizeObserver function we built the other day.

containers.forEach(async container => { // ... const observer = resizeObserver(container, { callback () { layout({colGap, items}) } }) })

That’s pretty much it! You now have a robust masonry grid that you can use in every working browser that supports CSS Grid!

Exciting, isn’t it? This implementation is so simple to use!

Masonry grid with Splendid Labz

If you’re not adverse to using code built by others, maybe you might want to consider grabbing the one I’ve built for you in Splendid Labz.

To do that, install the helper library and add the necessary code:

# Installing the library npm install @splendidlabz/styles /* Import all layouts code */ @import '@splendidlabz/layouts'; // Use the masonry script import { masonry } from '@splendidlabz/styles/scripts' masonry()

One last thing: I’ve been building a ton of tools to help make web development much easier for you and me. I’ve parked them all under the Splendid Labz brand — and one of these examples is this masonry grid I showed you today.

If you love this, you might be interested in other layout utilities that makes layout super simple to build.

Now, I hope you have enjoyed this article today. Go unleash your new CSS masonry grid if you wish to, and all the best!

Making a Masonry Layout That Works Today originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

How to Discover a CSS Trick

Css Tricks - Fri, 07/25/2025 - 3:48am

Do we invent or discover CSS tricks? Michelangelo described his sculpting process as chiseling away superfluous material to reveal the sculpture hidden inside the marble, and Stephen King says his ideas are pre-existing things he locates and uncovers “like fossils in the ground.” Paragraph one is early for me to get pretentious enough to liken myself to those iconic creative forces, but my work on CSS-Tricks feels like “discovering,” not “inventing,” secret synergies between CSS features, which have been eyeing each other from disparate sections of the MDN web docs and patiently waiting for someone to let them dance together in front of the world.

Matchmaking for CSS features

A strategy for finding unexpected alliances between CSS features to achieve the impossible is recursive thinking, which I bring to the CSS world from my engineering background. When you build recursive logic, you need to find an escape hatch to avoid infinite recursion, and this inception-style mindset helps me identify pairings of CSS features that seem at odds with each other yet work together surprisingly well. Take these examples from my CSS experiments:

Accepting there is nothing new under the sun

Indeed, Mark Twain thought new ideas don’t exist — he described them as illusions we create by combining ideas that have always existed, turning and overlaying them in a “mental kaleidoscope” to “make new and curious combinations.” It doesn’t mean creating is easy. No more than a safe can be cracked just by knowing the possible digits.

This brings back memories of playing Space Quest III as a kid because after you quit the game, it would output smart-aleck command-line messages, one of which was: “Remember, we did it all with ones and zeros.” Perhaps the point of the mock inspirational tone is that we likely will not be able to sculpt like Michelangelo or make a bestselling game, even if we were given the same materials and tools (is this an inspirational piece or what?). However, understanding the limits of what creators do is the foundation for cracking the combination of creativity to open the door to somewhere we haven’t been. And one truth that helps with achieving magic with CSS is that its constraints help breed creativity.

Embracing limitations

Being asked “Why would you do that in CSS when you could just use JavaScript?” is like if you asked me: “Why would you write a poem when it’s easier to write prose?” Samuel Coleridge defined prose as “words in their best order,” but poetry as “the best words in the best order.” If you think about it, the difference between prose and poetry is that the latter is based on increased constraints, which force us to find unexpected connections between ideas.

Similarly, the artist Phil Hansen learned that embracing limitation could drive creativity after he suffered permanent nerve damage in his hand, causing it to jitter, which prevented him from drawing the way he had in the past. His early experiments using this new mindset included limiting himself to creating a work using only 80 cents’ worth of supplies. This dovetails with the quote from Antoine de Saint-Exupéry often cited in web design, which says that perfection is achieved when there is nothing left to take away.

Embracing nothingness

The interesting thing about web design is how much it blends art and science. In both art and science, we challenge assumptions about whether commonsense relationships of cause and effect truly exist. Contrary to the saying in vernacular that “you can’t prove a negative,” we can. It’s not necessarily harder than proving a positive. So, in keeping with the discussion above of embracing limitations and removing the superfluous until a creation reveals itself, many of my article ideas prove a negative by challenging the assumption that one thing is necessary to produce another.

Going to extremes

Sometimes we can make a well-worn idea new again by taking it to the extreme. Seth Godin coined the term “edgecraftto describe a technique for generating ideas by pushing a competitive advantage as far to the edge as the market dares us to go. Similarly, sometimes you can take an old CSS feature that people have seen before, but push it further than anyone else to create something unique. For example:

  • CSS-Tricks covered checkbox hacks and radio button hacks back in 2011. But in 2021, I decided to see if I could use hundreds of radio button hacks using HTML generated with Pug to create a working Sudoku app. At one point, I found out that Chrome dev tools can display an infinite spinner of death when you throw too much generated CSS at it, which meant I had to limit myself to a 4×4 Sudoku, but that taught me more about what CSS can do and what it can’t.
  • The :target selector has existed since the 2000s. But in 2024, I took it to the extreme by using HAML to render the thousands of possible states of Tic Tac Toe to create a game with a computer opponent in pure CSS. At one point, CodePen refused to output as much HTML as I had asked it to, but it’s a fun way for newcomers to learn an important CSS feature; more engaging in my opinion than a table of contents demo.
Creating CSS outsider art

Chris Coyier has written about his distaste for the gatekeeping agenda hidden behind the question of whether CSS is a programming language. If CSS isn’t deemed as “real” programming, that can be used as an excuse to hold CSS experts in less esteem than people who code in imperative languages, which leads to unfair pay and toxic workplace dynamics.

But maybe the other side always seems greener due to the envy radiating from the people on that side, because as a full-stack engineer who completed a computer science degree, I always felt left out of the front-end conversations. It didn’t feel right to put “full stack developer” on my résumé when the creation of everything users can see in a web app seemed mysterious to me.

And maybe it wasn’t just psychosomatic that CSS made my head hurt compared to other types of coding because research indicates if you do fMRIs on people who are engaged in design tasks, you see that design cognition appears to involve a unique cognitive profile compared to conventional problem-solving, reflected in the areas of the brain that light up on the fMRIs. Studies show that the brain’s structure changes as people get better at different types of jobs. The brain’s structural plasticity is reminiscent of the way different muscles grow more pronounced with different types of exercise, but achieving what some of my colleagues could with CSS when my brain had been trained for decades on imperative logic felt about as approachable as lifting a car over my head.

The intimidation I felt from CSS started to change when I learned about the checkbox hack because I could relate to hiding and showing divs based on checkboxes, which was routine in my work in the back of the front-end. My designer workmate challenged me to make a game in one night using just CSS. I came up with a pure text adventure game made out of radio button hacks. Since creative and curious people are more sensitive to novel stimuli, the design experts on my team were enthralled by my primitive demo, not because it was cutting-edge gameplay but because it was something they had never seen before. My engineering background was now an asset rather than a hindrance in the unique outsider perspective I could bring to the world of CSS. I was hooked.

The hack I found to rewire my brain to become more CSS-friendly was to find analogies in CSS to the type of problem-solving I was more familiar with from imperative programming:

So if you are still learning web development and CSS (ultimately, we are all still learning), instead of feeling imposter syndrome, consider that the very thing that makes you feel like an outsider could be what enables you to bring something unique to your usage of CSS.

Finding the purpose

Excited as I was when my CSS hacking ended up providing the opportunity to publish my experiments on CSS-Tricks, the first comment on the first hack I published on CSS-Tricks was a generic, defeatist “Why would you do that?” criticism. The other comments popped up and turned out to be more supportive, and I said in a previous article that I’ve made my peace with the fact that not everybody will like my articles. However, this is the second article in which I’ve brought up the critical comment from back in 2021. Hmm…

Surely it wasn’t the reason I didn’t write another CSS-Tricks article for years. And it’s probably a coincidence that when I returned to CSS-Tricks last year, my first new article was a CSS hack that lends itself to accessibility after the person who left the negative comment about my first article seemed to have a bee in their bonnet about checkbox hacks breaking accessibility, even in fun CSS games not intended for production. Then again, limiting myself to CSS hacking that enables accessibility became a source of inspiration. We can all do with a reminder to at all times empathize with users who require screen readers, even when we are doing wacky experimental stuff, because we need to embrace the limitations not just of CSS but of our audience.

I suppose the reason the negative comment continues to rankle with me is that I agree that clarifying the relevance and purpose of a CSS trick is important. And yet, if I’m right in saying a CSS trick is more like something we discover than something we make, then it’s like finding a beautiful feather when we go for a walk. At first, we pick it up just because we can, but if I bring you with me on the journey that led to the discovery, then you can help me decide whether the significance is that the feather we discovered makes a great quill or reveals that a rare species of bird lives in this region.

It’s a journey versus destination thing to share the failures that led to compromises and the limitations I came up against when pushing the boundaries of CSS. When I bring you along on the route to the curious item I found, rather than just showing you that item, then after we part ways, you might retrace the steps and try a different fork in the path we followed, which could lead you to discover your own CSS trick.

How to Discover a CSS Trick originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Atomic Design Certification Course

Css Tricks - Thu, 07/24/2025 - 6:16am

Brad Frost introduced the “Atomic Design” concept wayyyy back in 2013. He even wrote a book on it. And we all took notice, because that term has been part of our lexicon ever since.

It’s a nice way to divide web designs into separate layers of concern, leaning into biology terms to help scope their context by size:

  1. Atoms
  2. Molecules
  3. Organisms
  4. Templates
  5. Pages

Atoms are part of molecules, which are part of organisms, which make up templates, which become full-blown pages. It’s composable design that’s centered on consistency, reusability, and maintainability. Beautiful. We’ve covered this a bunch over the years.

Want to get fully versed in how it works? If so, you’re in luck because Brad and his brother, Ian, are in the process of publishing an entire online course about Atomic Design. It’s in presale for $50 (with discounts for teams).

Normally, I like to jump into a course and check it out before sharing it. But this is Brad and all he does is wonderful things. For example:

Oh, and his newsletter is pretty awesome, too. And I’m sure I’m leaving out more things he has floating around the web, but you get the point: he’s incredibly knowledgeable on the topic, is a highly-effective educator and speaker, and most importantly, has an infectious positive posture about him.

I know the Atomic Design course will be just as awesome. Preordered!

Atomic Design Certification Course originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

New business wanted

QuirksBlog - Thu, 09/30/2021 - 12:22am

Last week Krijn and I decided to cancel performance.now() 2021. Although it was the right decision it leaves me in financially fairly dire straits. So I’m looking for new jobs and/or donations.

Even though the Corona trends in NL look good, and we could probably have brought 350 people together in November, we cannot be certain: there might be a new flare-up. More serious is the fact that it’s very hard to figure out how to apply the Corona checks Dutch government requires, especially for non-EU citizens. We couldn’t figure out how UK and US people should be tested, and for us that was the straw that broke the camel’s back. Cancelling the conference relieved us of a lot of stress.

Still, it also relieved me of a lot of money. This is the fourth conference in a row we cannot run, and I have burned through all my reserves. That’s why I thought I’d ask for help.

So ...

Has QuirksMode.org ever saved you a lot of time on a project? Did it advance your career? If so, now would be a great time to make a donation to show your appreciation.

I am trying my hand at CSS coaching. Though I had only few clients so far I found that I like it and would like to do it more. As an added bonus, because I’m still writing my CSS for JavaScripters book I currently have most of the CSS layout modules in my head and can explain them straight away — even stacking contexts.

Or if there’s any job you know of that requires a technical documentation writer with a solid knowledge of web technologies and the browser market, drop me a line. I’m interested.

Anyway, thanks for listening.

position: sticky, draft 1

QuirksBlog - Wed, 09/08/2021 - 7:44am

I’m writing the position: sticky part of my book, and since I never worked with sticky before I’m not totally sure if what I’m saying is correct.

This is made worse by the fact that there are no very clear tutorials on sticky. That’s partly because it works pretty intuitively in most cases, and partly because the details can be complicated.

So here’s my draft 1 of position: sticky. There will be something wrong with it; please correct me where needed.

The inset properties are top, right, bottom and left. (I already introduced this terminology earlier in the chapter.)

h3,h4,pre {clear: left} section.scroll-container { border: 1px solid black; width: 300px; height: 250px; padding: 1em; overflow: auto; --text: 'scroll box'; float: left; clear: left; margin-right: 0.5em; margin-bottom: 1em; position: relative; font-size: 1.3rem; } .container,.outer-container { border: 1px solid black; padding: 1em; position: relative; --text: 'container'; } .outer-container { --text: 'outer container'; } :is(.scroll-container,.container,.outer-container):before { position: absolute; content: var(--text); top: 0.2em; left: 0.2em; font-size: 0.8rem; } section.scroll-container h2 { position: sticky; top: 0; background: white; margin: 0 !important; color: inherit !important; padding: 0.5em !important; border: 1px solid; font-size: 1.4rem !important; } .nowrap p { white-space: nowrap; } Introduction

position: sticky is a mix of relative and fixed. A sticky box takes its normal position in the flow, as if it had position: relative, but if that position scrolls out of view the sticky box remains in a position defined by its inset properties, as if it has position: fixed. A sticky box never escapes its container, though. If the container start or end scrolls past the sticky box abandons its fixed position and sticks to the top or the bottom of its container.

It is typically used to make sure that headers remain in view no matter how the user scrolls. It is also useful for tables on narrow screens: you can keep headers or the leftmost table cells in view while the user scrolls.

Scroll box and container

A sticky box needs a scroll box: a box that is able to scroll. By default this is the browser window — or, more correctly, the layout viewport — but you can define another scroll box by setting overflow on the desired element. The sticky box takes the first ancestor that could scroll as its scroll box and calculates all its coordinates relative to it.

A sticky box needs at least one inset property. These properties contain vital instructions, and if the sticky box doesn’t receive them it doesn’t know what to do.

A sticky box may also have a container: a regular HTML element that contains the sticky box. The sticky box will never be positioned outside this container, which thus serves as a constraint.

The first example shows this set-up. The sticky <h2> is in a perfectly normal <div>, its container, and that container is in a <section> that is the scroll box because it has overflow: auto. The sticky box has an inset property to provide instructions. The relevant styles are:

section.scroll-container { border: 1px solid black; width: 300px; height: 300px; overflow: auto; padding: 1em; } div.container { border: 1px solid black; padding: 1em; } section.scroll-container h2 { position: sticky; top: 0; } The rules Sticky header

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

Now let’s see exactly what’s going on.

A sticky box never escapes its containing box. If it cannot obey the rules that follow without escaping from its container, it instead remains at the edge. Scroll down until the container disappears to see this in action.

A sticky box starts in its natural position in the flow, as if it has position: relative. It thus participates in the default flow: if it becomes higher it pushes the paragraphs below it downwards, just like any other regular HTML element. Also, the space it takes in the normal flow is kept open, even if it is currently in fixed position. Scroll down a little bit to see this in action: an empty space is kept open for the header.

A sticky box compares two positions: its natural position in the flow and its fixed position according to its inset properties. It does so in the coordinate frame of its scroll box. That is, any given coordinate such as top: 20px, as well as its default coordinates, is resolved against the content box of the scroll box. (In other words, the scroll box’s padding also constrains the sticky box; it will never move up into that padding.)

A sticky box with top takes the higher value of its top and its natural position in the flow, and positions its top border at that value. Scroll down slowly to see this in action: the sticky box starts at its natural position (let’s call it 20px), which is higher than its defined top (0). Thus it rests at its position in the natural flow. Scrolling up a few pixels doesn’t change this, but once its natural position becomes less than 0, the sticky box switches to a fixed layout and stays at that position.

The sticky box has bottom: 0

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Sticky header

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

It does the same for bottom, but remember that a bottom is calculated relative to the scroll box’s bottom, and not its top. Thus, a larger bottom coordinate means the box is positioned more to the top. Now the sticky box compares its default bottom with the defined bottom and uses the higher value to position its bottom border, just as before.

With left, it uses the higher value of its natural position and to position its left border; with right, it does the same for its right border, bearing in mind once more that a higher right value positions the box more to the left.

If any of these steps would position the sticky box outside its containing box it takes the position that just barely keeps it within its containing box.

Details Sticky header

Very, very long line of content to stretch up the container quite a bit

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

The four inset properties act independently of one another. For instance the following box will calculate the position of its top and left edge independently. They can be relative or fixed, depending on how the user scrolls.

p.testbox { position: sticky; top: 0; left: 0; }

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

The sticky box has top: 0; bottom: 0

Regular content

Regular content

Regular content

Regular content

Sticky header

Regular content

Regular content

Regular content

Regular content

Regular content

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

Setting both a top and a bottom, or both a left and a right, gives the sticky box a bandwidth to move in. It will always attempt to obey all the rules described above. So the following box will vary between 0 from the top of the screen to 0 from the bottom, taking its default position in the flow between these two positions.

p.testbox { position: sticky; top: 0; bottom: 0; } No container

Regular content

Regular content

Sticky header

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

So far we put the sticky box in a container separate from the scroll box. But that’s not necessary. You can also make the scroll box itself the container if you wish. The sticky element is still positioned with respect to the scroll box (which is now also its container) and everything works fine.

Several containers Sticky header

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Content outside container

Content outside container

Content outside outer container

Content outside outer container

Or the sticky item can be several containers removed from its scroll box. That’s fine as well; the positions are still calculated relative to the scroll box, and the sticky box will never leave its innermost container.

Changing the scroll box Sticky header

The container has overflow: auto.

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Content outside container

Content outside container

Content outside container

One feature that catches many people (including me) unaware is giving the container an overflow: auto or hidden. All of a sudden it seems the sticky header doesn’t work any more.

What’s going on here? An overflow value of auto, hidden, or scroll makes an element into a scroll box. So now the sticky box’s scroll box is no longer the outer element, but the inner one, since that is now the closest ancestor that is able to scroll.

The sticky box appears to be static, but it isn’t. The crux here is that the scroll box could scroll, thanks to its overflow value, but doesn’t actually do so because we didn’t give it a height, and therefore it stretches up to accomodate all of its contents.

Thus we have a non-scrolling scroll box, and that is the root cause of our problems.

As before, the sticky box calculates its position by comparing its natural position relative to its scroll box with the one given by its inset properties. Point is: the sticky box doesn’t scroll relative to its scroll box, so its position always remains the same. Where in earlier examples the position of the sticky element relative to the scroll box changed when we scrolled, it no longer does so, because the scroll box doesn’t scroll. Thus there is no reason for it to switch to fixed positioning, and it stays where it is relative to its scroll box.

The fact that the scroll box itself scrolls upward is irrelevant; this doesn’t influence the sticky box in the slightest.

Sticky header

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Regular content

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

Content outside container

One solution is to give the new scroll box a height that is too little for its contents. Now the scroll box generates a scrollbar and becomes a scrolling scroll box. When we scroll it the position of the sticky box relative to its scroll box changes once more, and it switches from fixed to relative or vice versa as required.

Minor items

Finally a few minor items:

  • It is no longer necessary to use position: -webkit-sticky. All modern browsers support regular position: sticky. (But if you need to cater to a few older browsers, retaining the double syntax doesn’t hurt.)
  • Chrome (Mac) does weird things to the borders of the sticky items in these examples. I don’t know what’s going on and am not going to investigate.

Breaking the web forward

QuirksBlog - Thu, 08/12/2021 - 5:19am

Safari is holding back the web. It is the new IE, after all. In contrast, Chrome is pushing the web forward so hard that it’s starting to break. Meanwhile web developers do nothing except moan and complain. The only thing left to do is to pick our poison.

blockquote { font-size: inherit; font-family: inherit; } blockquote p { font-size: inherit; font-family: inherit; } Safari is the new IE

Recently there was yet another round of “Safari is the new IE” stories. Once Jeremy’s summary and a short discussion cleared my mind I finally figured out that Safari is not IE, and that Safari’s IE-or-not-IE is not the worst problem the web is facing.

Perry Sun argues that for developers, Safari is crap and outdated, emulating the old IE of fifteen years ago in this respect. He also repeats the theory that Apple is deliberately starving Safari of features in order to protect the app store, and thus its bottom line. We’ll get back to that.

The allegation that Safari is holding back web development by its lack of support for key features is not new, but it’s not true, either. Back fifteen years ago IE held back the web because web developers had to cater to its outdated technology stack. “Best viewed with IE” and all that. But do you ever see a “Best viewed with Safari” notice? No, you don’t. Another browser takes that special place in web developers’ hearts and minds.

Chrome is the new IE, but in reverse

Jorge Arango fears we’re going back to the bad old days with “Best viewed in Chrome.” Chris Krycho reinforces this by pointing out that, even though Chrome is not the standard, it’s treated as such by many web developers.

“Best viewed in Chrome” squares very badly with “Safari is the new IE.” Safari’s sad state does not force web developers to restrict themselves to Safari-supported features, so it does not hold the same position as IE.

So I propose to lay this tired old meme to rest. Safari is not the new IE. If anything it’s the new Netscape 4.

Meanwhile it is Chrome that is the new IE, but in reverse.

Break the web forward

Back in the day, IE was accused of an embrace, extend, and extinguish strategy. After IE6 Microsoft did nothing for ages, assuming it had won the web. Thanks to web developers taking action in their own name for the first (and only) time, IE was updated once more and the web moved forward again.

Google learned from Microsoft’s mistakes and follows a novel embrace, extend, and extinguish strategy by breaking the web and stomping on the bits. Who cares if it breaks as long as we go forward. And to hell with backward compatibility.

Back in 2015 I proposed to stop pushing the web forward, and as expected the Chrome devrels were especially outraged at this idea. It never went anywhere. (Truth to tell: I hadn’t expected it to.)

I still think we should stop pushing the web forward for a while until we figure out where we want to push the web forward to — but as long as Google is in charge that won’t happen. It will only get worse.

On alert

A blog storm broke out over the decision to remove alert(), confirm() and prompt(), first only the cross-origin variants, but eventually all of them. Jeremy and Chris Coyier already summarised the situation, while Rich Harris discusses the uses of the three ancient modals, especially when it comes to learning JavaScript.

With all these articles already written I will only note that, if the three ancient modals are truly as horrendous a security issue as Google says they are it took everyone a bloody long time to figure that out. I mean, they turn 25 this year.

Although it appears Firefox and Safari are on board with at least the cross-origin part of the proposal, there is no doubt that it’s Google that leads the charge.

From Google’s perspective the ancient modals have one crucial flaw quite apart from their security model: they weren’t invented there. That’s why they have to be replaced by — I don’t know what, but it will likely be a very complicated API.

Complex systems and arrogant priests rule the web

Thus the new embrace, extend, and extinguish is breaking backward compatibility in order to make the web more complicated. Nolan Lawson puts it like this:

we end up with convoluted specs like Service Worker that you need a PhD to understand, and yet we still don't have a working <dialog> element.

In addition, Google can be pretty arrogant and condescending, as Chris Ferdinandi points out.

The condescending “did you actually read it, it’s so clear” refrain is patronizing AF. It’s the equivalent of “just” or “simply” in developer documentation.

I read it. I didn’t understand it. That’s why I asked someone whose literal job is communicating with developers about changes Chrome makes to the platform.

This is not isolated to one developer at Chrome. The entire message thread where this change was surfaced is filled with folks begging Chrome not to move forward with this proposal because it will break all-the-things.

If you write documentation or a technical article and nobody understands it, you’ve done a crappy job. I should know; I’ve been writing this stuff for twenty years.

Extend, embrace, extinguish. And use lots of difficult words.

Patience is a virtue

As a reaction to web dev outcry Google temporarily halted the breaking of the web. That sounds great but really isn’t. It’s just a clever tactical move.

I saw this tactic in action before. Back in early 2016 Google tried to break the de-facto standard for the mobile visual viewport that I worked very hard to establish. I wrote a piece that resonated with web developers, whose complaints made Google abandon the plan — temporarily. They tried again in late 2017, and I again wrote an article, but this time around nobody cared and the changes took effect and backward compatibility was broken.

So the three ancient modals still have about 12 to 18 months to live. Somewhere in late 2022 to early 2023 Google will try again, web developers will be silent, and the modals will be gone.

The pursuit of appiness

But why is Google breaking the web forward at such a pace? And why is Apple holding it back?

Safari is kept dumb to protect the app store and thus revenue. In contrast, the Chrome team is pushing very hard to port every single app functionality to the browser. Ages ago I argued we should give up on this, but of course no one listened.

When performing Valley Kremlinology, it is useful to see Google policies as stemming from a conflict between internal pro-web and anti-web factions. We web developers mainly deal with the pro-web faction, the Chrome devrel and browser teams. On the other hand, the Android team is squarely in the anti-web camp.

When seen in this light the pro-web camp’s insistence on copying everything appy makes excellent sense: if they didn’t Chrome would lag behind apps and the Android anti-web camp would gain too much power. While I prefer the pro-web over the anti-web camp, I would even more prefer the web not to be a pawn in an internal Google power struggle. But it has come to that, no doubt about it.

Solutions?

Is there any good solution? Not really.

Jim Nielsen feels that part of the issue is the lack of representation of web developers in the standardization process. That sounds great but is proven not to work.

Three years ago Fronteers and I attempted to get web developers represented and were met with absolute disinterest. Nobody else cared even one shit, and the initiative sank like a stone.

So a hypothetical web dev representative in W3C is not going to work. Also, the organisational work would involve a lot of unpaid labour, and I, for one, am not willing to do it again. Neither is anyone else. So this is not the solution.

And what about Firefox? Well, what about it? Ten years ago it made a disastrous mistake by ignoring the mobile web for way too long, then it attempted an arrogant and uninformed come-back with Firefox OS that failed, and its history from that point on is one long slide into obscurity. That’s what you get with shitty management.

Pick your poison

So Safari is trying to slow the web down. With Google’s move-fast-break-absofuckinglutely-everything axiom in mind, is Safari’s approach so bad?

Regardless of where you feel the web should be on this spectrum between Google and Apple, there is a fundamental difference between the two.

We have the tools and procedures to manage Safari’s disinterest. They’re essentially the same as the ones we deployed against Microsoft back in the day — though a fundamental difference is that Microsoft was willing to talk while Apple remains its old haughty self, and its “devrels” aren’t actually allowed to do devrelly things such as managing relations with web developers. (Don’t blame them, by the way. If something would ever change they’re going to be our most valuable internal allies — just as the IE team was back in the day.)

On the other hand, we have no process for countering Google’s reverse embrace, extend, and extinguish strategy, since a section of web devs will be enthusiastic about whatever the newest API is. Also, Google devrels talk. And talk. And talk. And provide gigs of data that are hard to make sense of. And refer to their proprietary algorithms that “clearly” show X is in the best interest of the web — and don’t ask questions! And make everything so fucking complicated that we eventually give up and give in.

So pick your poison. Shall we push the web forward until it’s broken, or shall we break it by inaction? What will it be? Privately, my money is on Google. So we should say goodbye to the old web while we still can.

Custom properties and @property

QuirksBlog - Wed, 07/21/2021 - 3:18am

You’re reading a failed article. I hoped to write about @property and how it is useful for extending CSS inheritance considerably in many different circumstances. Alas, I failed. @property turns out to be very useful for font sizes, but does not even approach the general applicability I hoped for.

Grandparent-inheriting

It all started when I commented on what I thought was an interesting but theoretical idea by Lea Verou: what if elements could inherit the font size of not their parent, but their grandparent? Something like this:

div.grandparent { /* font-size could be anything */ } div.parent { font-size: 0.4em; } div.child { font-size: [inherit from grandparent in some sort of way]; font-size: [yes, you could do 2.5em to restore the grandparent's font size]; font-size: [but that's not inheriting, it's just reversing a calculation]; font-size: [and it will not work if the parent's font size is also unknown]; }

Lea told me this wasn’t a vague idea, but something that can be done right now. I was quite surprised — and I assume many of my readers are as well — and asked for more information. So she wrote Inherit ancestor font-size, for fun and profit, where she explained how the new Houdini @property can be used to do this.

This was seriously cool. Also, I picked up a few interesting bits about how CSS custom properties and Houdini @property work. I decided to explain these tricky bits in simple terms — mostly because I know that by writing an explanation I myself will understand them better — and to suggest other possibilities for using Lea’s idea.

Alas, that last objective is where I failed. Lea’s idea can only be used for font sizes. That’s an important use case, but I had hoped for more. The reasons why it doesn’t work elsewhere are instructive, though.

Tokens and values

Let’s consider CSS custom properties. What if we store the grandparent’s font size in a custom property and use that in the child?

div.grandparent { /* font-size could be anything */ --myFontSize: 1em; } div.parent { font-size: 0.4em; } div.child { font-size: var(--myFontSize); /* hey, that's the grandparent's font size, isn't it? */ }

This does not work. The child will have the same font size as the parent, and ignore the grandparent. In order to understand why we need to understand how custom properties work. What does this line of CSS do?

--myFontSize: 1em;

It sets a custom property that we can use later. Well duh.

Sure. But what value does this custom property have?

... errr ... 1em?

Nope. The answer is: none. That’s why the code example doesn’t work.

When they are defined, custom properties do not have a value or a type. All that you ordered the browsers to do is to store a token in the variable --myFontSize.

This took me a while to wrap my head around, so let’s go a bit deeper. What is a token? Let’s briefly switch to JavaScript to explain.

let myVar = 10;

What’s the value of myVar in this line? I do not mean: what value is stored in the variable myVar, but: what value does the character sequence myVar have in that line of code? And what type?

Well, none. Duh. It’s not a variable or value, it’s just a token that the JavaScript engine interprets as “allow me to access and change a specific variable” whenever you type it.

CSS custom properties also hold such tokens. They do not have any intrinsic meaning. Instead, they acquire meaning when they are interpreted by the CSS engine in a certain context, just as the myVar token is in the JavaScript example.

So the CSS custom property contains the token 1em without any value, without any type, without any meaning — as yet.

You can use pretty any bunch of characters in a custom property definition. Browsers make no assumptions about their validity or usefulness because they don’t yet know what you want to do with the token. So this, too, is a perfectly fine CSS custom property:

--myEgoTrip: ppk;

Browsers shrug, create the custom property, and store the indicated token. The fact that ppk is invalid in all CSS contexts is irrelevant: we haven’t tried to use it yet.

It’s when you actually use the custom property that values and types are assigned. So let’s use it:

background-color: var(--myEgoTrip);

Now the CSS parser takes the tokens we defined earlier and replaces the custom property with them:

background-color: ppk;

And only NOW the tokens are read and intrepreted. In this case that results in an error: ppk is not a valid value for background-color. So the CSS declaration as a whole is invalid and nothing happens — well, technically it gets the unset value, but the net result is the same. The custom property itself is still perfectly valid, though.

The same happens in our original code example:

div.grandparent { /* font-size could be anything */ --myFontSize: 1em; /* just a token; no value, no meaning */ } div.parent { font-size: 0.4em; } div.child { font-size: var(--myFontSize); /* becomes */ font-size: 1em; /* hey, this is valid CSS! */ /* Right, you obviously want the font size to be the same as the parent's */ /* Sure thing, here you go */ }

In div.child he tokens are read and interpreted by the CSS parser. This results in a declaration font-size: 1em;. This is perfectly valid CSS, and the browsers duly note that the font size of this element should be 1em.

font-size: 1em is relative. To what? Well, to the parent’s font size, of course. Duh. That’s how CSS font-size works.

So now the font size of the child becomes the same as its parent’s, and browsers will proudly display the child element’s text in the same font size as the parent element’s while ignoring the grandparent.

This is not what we wanted to achieve, though. We want the grandparent’s font size. Custom properties — by themselves — don’t do what we want. We have to find another solution.

@property

Lea’s article explains that other solution. We have to use the Houdini @property rule.

@property --myFontSize { syntax: "<length>"; initial-value: 0; inherits: true; } div { border: 1px solid; padding: 1em; } div.grandparent { /* font-size could be anything */ --myFontSize: 1em; } div.parent { font-size: 0.4em; } div.child { font-size: var(--myFontSize); }

Now it works. Wut? Yep — though only in Chrome so far.

@property --myFontSize { syntax: ""; initial-value: 0; inherits: true; } section.example { max-width: 500px; } section.example div { border: 1px solid; padding: 1em; } div.grandparent { font-size: 23px; --myFontSize: 1em; } div.parent { font-size: 0.4em; } div.child { font-size: var(--myFontSize); } This is the grandparent This is the parent This is the child

What black magic is this?

Adding the @property rule changes the custom property --myFontSize from a bunch of tokens without meaning to an actual value. Moreover, this value is calculated in the context it is defined in — the grandfather — so that the 1em value now means 100% of the font size of the grandfather. When we use it in the child it still has this value, and therefore the child gets the same font size as the grandfather, which is exactly what we want to achieve.

(The variable uses a value from the context it’s defined in, and not the context it’s executed in. If, like me, you have a grounding in basic JavaScript you may hear “closures!” in the back of your mind. While they are not the same, and you shouldn’t take this apparent equivalency too far, this notion still helped me understand. Maybe it’ll help you as well.)

Unfortunately I do not quite understand what I’m doing here, though I can assure you the code snippet works in Chrome — and will likely work in the other browsers once they support @property.

Misson completed — just don’t ask me how.

Syntax

You have to get the definition right. You need all three lines in the @property rule. See also the specification and the MDN page.

@property --myFontSize { syntax: "<length>"; initial-value: 0; inherits: true; }

The syntax property tells browsers what kind of property it is and makes parsing it easier. Here is the list of possible values for syntax, and in 99% of the cases one of these values is what you need.

You could also create your own syntax, e.g. syntax: "ppk | <length>"

Now the ppk keyword and any sort of length is allowed as a value.

Note that percentages are not lengths — one of the many things I found out during the writing of this article. Still, they are so common that a special value for “length that may be a percentage or may be calculated using percentages” was created:

syntax: "<length-percentage>"

Finally, one special case you need to know about is this one:

syntax: "*"

MDN calls this a universal selector, but it isn’t, really. Instead, it means “I don’t know what syntax we’re going to use” and it tells browsers not to attempt to interpret the custom property. In our case that would be counterproductive: we definitely want the 1em to be interpreted. So our example doesn’t work with syntax: "*".

initial-value and inherits

An initial-value property is required for any syntax value that is not a *. Here that’s simple: just give it an initial value of 0 — or 16px, or any absolute value. The value doesn’t really matter since we’re going to overrule it anyway. Still, a relative value such as 1em is not allowed: browsers don’t know what the 1em would be relative to and reject it as an initial value.

Finally, inherits: true specifies that the custom property value can be inherited. We definitely want the computed 1em value to be inherited by the child — that’s the entire point of this experiment. So we carefully set this flag to true.

Other use cases

So far this article merely rehashed parts of Lea’s. Since I’m not in the habit of rehashing other people’s articles my original plan was to add at least one other use case. Alas, I failed, though Lea was kind enough to explain why each of my ideas fails.

Percentage of what?

Could we grandfather-inherit percentual margins and paddings? They are relative to the width of the parent of the element you define them on, and I was wondering if it might be useful to send the grandparent’s margin on to the child just like the font size. Something like this:

@property --myMargin { syntax: "<length-percentage>"; initial-value: 0; inherits: true; } div.grandparent { --myMargin: 25%; margin-left: var(--myMargin); } div.parent { font-size: 0.4em; } div.child { margin-left: var(--myMargin); /* should now be 25% of the width of the grandfather's parent */ /* but isn't */ }

Alas, this does not work. Browsers cannot resolve the 25% in the context of the grandparent, as they did with the 1em, because they don’t know what to do.

The most important trick for using percentages in CSS is to always ask yourself: “percentage of WHAT?”

That’s exactly what browsers do when they encounter this @property definition. 25% of what? The parent’s font size? Or the parent’s width? (This is the correct answer, but browsers have no way of knowing that.) Or maybe the width of the element itself, for use in background-position?

Since browsers cannot figure out what the percentage is relative to they do nothing: the custom property gets the initial value of 0 and the grandfather-inheritance fails.

Colours

Another idea I had was using this trick for the grandfather’s text colour. What if we store currentColor, which always has the value of the element’s text colour, and send it on to the grandchild? Something like this:

@property --myColor { syntax: "<color>"; initial-value: black; inherits: true; } div.grandparent { /* color unknown */ --myColor: currentColor; } div.parent { color: red; } div.child { color: var(--myColor); /* should now have the same color as the grandfather */ /* but doesn't */ }

Alas, this does not work either. When the @property blocks are evaluated, and 1em is calculated, currentColor specifically is not touched because it is used as an initial (default) value for some inherited SVG and CSS properties such as fill. Unfortunately I do not fully understand what’s going on, but Tab says this behaviour is necessary, so it is.

Pity, but such is life. Especially when you’re working with new CSS functionalities.

Conclusion

So I tried to find more possbilities for using Lea’s trick, but failed. Relative units are fairly sparse, especially when you leave percentages out of the equation. em and related units such as rem are the only ones, as far as I can see.

So we’re left with a very useful trick for font sizes. You should use it when you need it (bearing in mind that right now it’s only supported in Chromium-based browsers), but extending it to other declarations is not possible at the moment.

Many thanks to Lea Verou and Tab Atkins for reviewing and correcting an earlier draft of this article.

Let&#8217;s talk about money

QuirksBlog - Tue, 06/29/2021 - 1:23am

Let’s talk about money!

Let’s talk about how hard it is to pay small amounts online to people whose work you like and who could really use a bit of income. Let’s talk about how Coil aims to change that.

Taking a subscription to a website is moderately easy, but the person you want to pay must have enabled them. Besides, do you want to purchase a full subscription in order to read one or two articles per month?

Sending a one-time donation is pretty easy as well, but, again, the site owner must have enabled them. And even then it just gives them ad-hoc amounts that they cannot depend on.

Then there’s Patreon and Kickstarter and similar systems, but Patreon is essentially a subscription service while Kickstarter is essentially a one-time donation service, except that both keep part of the money you donate.

And then there’s ads ... Do we want small content creators to remain dependent on ads and thus support the entire ad ecosystem? I, personally, would like to get rid of them.

The problem today is that all non-ad-based systems require you to make conscious decisions to support someone — and even if you’re serious about supporting them you may forget to send in a monthly donation or to renew your subscription. It sort-of works, but the user experience can be improved rather dramatically.

That’s where Coil and the Web Monetization Standard come in.

Web Monetization

The idea behind Coil is that you pay for what you consume easily and automatically. It’s not a subscription - you only pay for what you consume. It’s not a one-time donation, either - you always pay when you consume.

Payments occur automatically when you visit a website that is also subscribed to Coil, and the amount you pay to a single site owner depends on the time you spend on the site. Coil does not retain any of your money, either — everything goes to the people you support.

In this series of four articles we’ll take a closer look at the architecture of the current Coil implementation, how to work with it right now, the proposed standard, and what’s going to happen in the future.

Overview

So how does Coil work right now?

Both the payer and the payee need a Coil account to send and receive money. The payee has to add a <meta> tag with a Coil payment pointer to all pages they want to monetize. The payer has to install the Coil extension in their browsers. You can see this extension as a polyfill. In the future web monetization will, I hope, be supported natively in all browsers.

Once that’s done the process works pretty much automatically. The extension searches for the <meta> tag on any site the user visits. If it finds one it starts a payment stream from payer to payee that continues for as long as the payer stays on the site.

The payee can use the JavaScript API to interact with the monetization stream. For instance, they can show extra content to paying users, or keep track of how much a user paid so far. Unfortunately these functionalities require JavaScript, and the hiding of content is fairly easy to work around. Thus it is not yet suited for serious business purposes, especially in web development circles.

This is one example of how the current system is still a bit rough around the edges. You’ll find more examples in the subsequent articles. Until the time browsers support the standard natively and you can determine your visitors’ monetization status server-side these rough bits will continue to exist. For the moment we will have to work with the system we have.

This article series will discuss all topics we touched on in more detail.

Start now!

For too long we have accepted free content as our birthright, without considering the needs of the people who create it. This becomes even more curious for articles and documentation that are absolutely vital to our work as web developers.

Take a look at this list of currently-monetized web developer sites. Chances are you’ll find a few people whose work you used in the past. Don’t they deserve your direct support?

Free content is not a right, it’s an entitlement. The sooner we internalize this, and start paying independent voices, the better for the web.

The only alternative is that all articles and documentation that we depend on will written by employees of large companies. And employees, no matter how well-meaning, will reflect the priorities and point of view of their employer in the long run.

So start now.

In order to support them you should invest a bit of time once and US$5 per month permanently. I mean, that’s not too much to ask, is it?

Continue

I wrote this article and its sequels for Coil, and yes, I’m getting paid. Still, I believe in what they are doing, so I won’t just spread marketing drivel. Initially it was unclear to me exactly how Coil works. So I did some digging, and the remaining parts of this series give a detailed description of how Coil actually works in practice.

For now the other three articles will only be available on dev.to. I just published part 2, which gives a high-level overview of how Coil works right now. Part 3 will describe the meta tag and the JavaScript API, and in part 4 we’ll take a look at the future, which includes a formal W3C standard. Those parts will be published next week and the week after that.

Wed, 12/31/1969 - 2:00pm
Syndicate content
©2003 - Present Akamai Design & Development.