🔥 Mastering CSS

Tuan Tran VanTuan Tran Van
49 min read

Are you ready to advance your CSS skills? Whether you are a seasoned pro or just starting out, you have all experienced those moments when your style sheets seem to have a mind of their own. This article will introduce you to some advanced CSS concepts, tips, and hacks that are sure to make your life easier and your designs impressive.

In this blog post, we are going to explore some awesome CSS hacks that will help you solve common challenges, improve your workflow, and add some extra pizzazz to your projects. These aren’t just any old tricks - they are practical, powerful, and perfect for UI developers like us who want to create stunning web experiences.

So, grab your favorite beverage, get comfy, and let’s dive into the world of CSS hacks!

Modern CSS One-Line Upgrades

CSS Flexbox

Check out this article to explore the details.

https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/

CSS Grid

Check out this handbook to explore everything about Gird CSS.

https://www.joshwcomeau.com/css/interactive-guide-to-grid/?ref=dailydev#grid-flow-2

aspect-ratio

Have you ever used a “padding hack“ to force an aspect ratio such as 16:9 video embeds? As of September 2021, the aspect-ratio property is stable in evergreen browsers and is the only property needed to define an aspect ratio.

For an HD video, you can just use an aspect-ratio: 16/9. For a perfect square, only aspect-ratio: 1 is required since the implied second value is also 1;

Of note, an applied aspect-ratio is forgiving and will allow context to take precedence. This means that when content causes the element to exceed the ratio in at least one dimension, the element will still grow or change shape to accommodate the content. To prevent or control this behavior, you can add additional dimension properties, like max-width, which may be necessary to avoid expanding out the flex or grid container.

object-fit

This is actually the oldest property on this list, but it solves an important issue and definitely fits the sentiment of a one-line upgrade.

The use of object-fit causes an image or other replaced elements to act as the container for its content and have those contents adopt resizing behavior similar to background-size.

While there are a few values available for object-fit, the following are the ones you are most likely to use:

  • cover - The image is resized to cover the element and maintain its aspect ratio so that the content is not distorted.

  • scale-down: The image resizes (if needed) within the element so that it is fully visible without being clipped and maintains its aspect ratio, which may lead to extra space (“letterboxing”) around the image if the image has a differently rendered aspect ratio.

In either case, object-fit is an excellent property pairing with aspect-ratio to ensure images are not distorted when you apply a custom aspect ratio.

margin-inline

One of many logical properties, margin-inline functions as a shorthand for setting the margin inline (left and right in horizontal writing modes).

The replacement here is simple:

/* Before */
margin-left: auto;
margin-right: auto;

/* After */
margin-inline: auto;

Logical properties have been available for a couple of years and now have support upwards of 98% (with occasional prefixing). Review the article from Ahmad Shadeed to learn more about using logical properties and their importance for sites with international audiences.

text-underline-offset

The use of text-underline-offset allows you to control the distance between the text baseline and the underline. This property has become a part of my standard reset, applied as follows:

a:not([class]) {
    text-underline-offset: 0.25em;
}

This offset can clear descenders and (subjectively) improve legibility, particularly when links are grouped in close proximity, such as in a bulleted list.

This upgrade may replace older hacks like a border or pseudo-element or given a gradient background, especially when used with its friend:

  • text-decoration-color: to change the underlying color

  • text-decoration-thickness: to change the underlined stroke thickness

outline-offset

Have you been using box-shadow or perhaps a pseudo-element to supply a custom outline when you wanted distance between the element and outline on focus?

Good news! The long-available outline-offset property may be the one you missed, and it enables pushing the outline away from the element with the positive value or pulling it into the element with the negative value.

In the demo, the gray solid line is the element border, and the blue dashed line is the outline being positioned via outline-offset.

Reminder: Outlines are not computed as parts of the element’s box size, so increasing the distance will not increase the amount of space an element occupies. This is similar to how box-shadow is rendered without impacting the element size as well.

scroll-margin-top/bottom

The scroll-margin set of properties (and corresponding scroll-padding) allows adding an offset to an element in the context of the scroll position. In other words, adding scroll-padding-top can increase the scroll offset above the element but doesn’t affect its layout position within the document.

Why is this useful? Well, it can alleviate issues caused by a sticky nav element covering content when an anchor link is activated. Using scroll-margin-top we can increase the space above the element when it is scrolled via navigation to account for the space occupied by the sticky nav.

If you have a navigation bar and an anchor link, the bar might obscure the target element. You can use scroll-margin-top to account for the height of the sticky header.

<nav class="sticky-header">Header</nav>
<div id="section1" class="scroll-target">Section 1</div>
.sticky-header {
  position: sticky;
  top: 0;
  height: 50px;
  background: #333;
  color: white;
}

.scroll-target {
  scroll-margin-top: 50px; /* Adds space equal to the sticky header's height */
}

When navigating to #section1, it will appear 50px below the top of the viewport, leaving space for the sticky header.

color-scheme

You may be familiar with the prefers-color-scheme media query to customize dark and light themes. The CSS property color-scheme is an opt-in to adapting browser UI elements, including form controls, scrollbars, and CSS system colors. The adaptation asks the browser to render those items with either a light or dark scheme, and the property allows defining a preference order.

If you’re enabling the adaptation of your entire application, set the following :root, which says to preference a dark theme (or flip the order to preference a light theme).

:root {
    color-scheme: dark light;
}

You can also define color-scheme on individual elements, such as adjusting form controls within an element with a dark background to improve contrast.

.dark-background {
    color-scheme: dark;
}

accent-color

If you have ever wanted to change the color of checkboxes or radio buttons, you have been seeking accent-color. With this property, you can modify the :checked the appearance of radio buttons or checkboxes and the filled-in states for both the progress element and range input. The browser’s default focus “halo“ may also be adjusted if you do not have another override.

Consider adding both accent-color and color-scheme to your baseline application styles for a quick win toward custom theme management.

width: fit-content

One of my favorite CSS hidden gems is the use of fit-content to “shrink wrap“ an element to its content.

Whereas you may have used an inline display value such as display: inline-block to reduce an element’s width to the content size, an upgrade to width: fit-content will achieve the same effect. The advantage of width: fit-content is that it leaves the display value available, thereby not changing the position of the element in the layout unless you adjust that as well. The computed box size will adjust to the dimensions created by fit-content.

Consider the secondary upgrade for this technique to the logical property equivalent of inline-size: fit-content.

overscroll-behavior

The default behavior of contained scroll regions - areas with limited dimensions where overflow is allowed to be scrolled - is that when the scroll runs out in the element, the scroll interaction passes to the background page. This can be jarring at best and frustrating at worst for your users.

Use of overscroll-behavior: contain will isolate the scrolling to the contained region, preventing continuing the scroll by moving it to the parent page once the scroll boundary is reached. This is useful in contexts such as a sidebar of navigation links, which may have an independent scroll from the main page content, which may be a long article or documentation page.

Scrollbar-Gutter & Scrollbar-Color

When a browser displays a scrollbar, the layout can shift as space is taken up. With scrollbar-gutter, you can preserve scrollbar space even before scrolling begins:

.scrollable {
  scrollbar-gutter: stable both-edges;
}

You can also style your scrollbars with scrollbar-color:

.scrollable {
  scrollbar-color: #444 #ccc;
}

This ensures a consistent look and prevents layout jumps.

What it’s good for ✅

  • scrollbar-gutter keeps layouts stable by reserving space for a scrollbar, preventing annoying shifts when the scrollbar appears.

  • scrollbar-color lets you style the scrollbar’s track and thumb, enhancing design consistency, especially for dark or themed UIs.

:: target-text

::target-text highlights text that’s reached by an internal link (for example, when clicking an anchor on the same page):

::target-text {
  background: yellow;
  color: black;
}

Readers immediately see the exact part of the text they’ve navigated to.

What it’s good for ✅

  • Highlights the exact text targeted by a link anchor, making it immediately clear where users have landed in long documents or articles.

transition-behavior

While transition-timing-function has served us well, transition-behavior introduces additional control over animations, such as reversing or pausing transitions without complex JavaScript. This paves the way for smoother UI interactions and advanced animation scenarios.

.card {
  transition-property: opacity, display;
  transition-duration: 0.25s;
  transition-behavior: allow-discrete;
}

.card.fade-out {
  opacity: 0;
  display: none;
}

What it’s good for ✅

  • Expands on basic transitions to offer reversible or more complex transitions without heavy scripting.

  • Useful for refined UI effects, interactive components, and unique animation scenarios.

object-position

When you apply it to the image object-fit - It will crop the image to the specified height & width in order to look sharp:

However, we can’t control which part of the image will be cropped, so the object position is crucial.

.test {
  height: 350px;
  width: 500px;
  object-fit: cover;
  object-position: bottom;
}

object-position: top;

As well as we can add: left or right. We can even be more precise when we specify two parameters like object-position: center bottom;

scope

The @scope at-rule introduces a revolutionary way to manage CSS specificity and encapsulation. It allows developers to define a specific scope for CSS selectors, making it easier to target elements within particular DOM subtrees without affecting that element outside the scope.

How it works: The @scope rule defines a boundary within which certain styles apply. This boundary is determined by a selector, and the styles within the @scope block only affect elements that match the selector and are descendants of the scoped element.

<div id="my-component">
  <p>This paragraph is inside the scope.</p>
</div>
<p>This paragraph is outside the scope.</p>
<style>
@scope (#my-component) {
  p {
    color: blue;
  }
}
</style>

In this example, only the paragraph inside the #my-component div will be colored blue. The paragraph outside remains unaffected.

Use cases:

  • Component-Based Architectures: @scope is the idea of styling components independently. Each component can have its own encapsulated styles, preventing conflicts between components and promoting code reuse.

  • Third-party Libraries: Control the styling of third-party libraries or embedded widgets without worrying their styles affecting your main application’s style.

  • Scoped CSS Modules: While CSS Modules and other build-time solutions offer similar functionality,@scope brings this capability is brought natively to the browser, simplifying workflows and potentially improving performance.

supports

The @supports rule in CSS is a feature-detection mechanism that allows developers to apply styles conditionally based on whether a specific CSS property is supported by the browser. This enables graceful degradation and progressive enhancement, ensuring that modern features are used when available while providing fallback styles for older browsers.

How it works: The @supports rule checks if a given CSS property or feature is supported by the browser. If the condition evaluates to true, the enclosed styles are applied.

@supports (display: flex) {
  .flex-container > * {
    text-shadow: 0 0 2px blue;
    float: none;
  }
}

In this example, the styles inside @supports will only apply if the browser supports display: flex, ensuring a modern layout while avoiding issues in unsupported environments.

Use Cases:

  • Progressive Enhancement: Apply modern styles while ensuring compatibility with older browsers.

  • Failure Detection for New CSS Properties: Check support for advanced CSS properties like content-visibility or aspect-ratio.

  • CSS Grid and Flexbox Fallbacks: Use @supports to apply grid or flexbox styles only when they are available while providing float-based fallbacks for older browsers.

  • Ensuring Browser Compatibility: This helps maintain a consistent design experience across different user environments.

content-visibility

The content-visibility property is a game-changer for performance optimization in CSS. It enables browsers to skip the rendering work for an element until it’s needed, significantly improving the loading and rendering performance of complex or large layouts.

How it works: When set to ‘auto’, content-visibility tells the browser that it can skip the rendering of the element’s content if they are not currently visible in the viewport. The browser still does the initial layout, but it doesn’t render the content until it’s needed.

section {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px;
}

In this example, sections that are not in the viewport will not have their contents rendered, speeding up the initial page load. The contain-intrinsic-size property provides an estimated size for the content, helping to prevent layout shifts as the content is loaded.

Use Cases:

  • Long Lists and Feeds: Optimize the performance of infinite scrolling lists or news feeds by only rendering the items currently visible in the viewport.

  • Complex layouts: Improved the initial load time of pages with complex layouts by deferring the rendering of off-screen sections until they are needed

  • Tabs and Accordions: Prevent the browser from rendering the hidden tab panels or accordion sections until they are activated, improving initial page load and interactivity.

  • Image-Heavy Pages: Combine content-visibility: auto with lazy-loading techniques to further optimize image loading and improve perceived performance.

keyframes

The @keyframes rule is a cornerstone of CSS animations, allowing developers to define the intermediate steps in a CSS animation sequence. This powerful feature enables the creation of complex, multi-step animation without relying on Javascript.

How it works: @keyframes defines a series of style changes that should occur at specified points during an animation. These keyframes can be defined using percentages of the animation duration or the keywords ‘from‘ and ‘to’.

@keyframes slide-in {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }

  50% {
    opacity: 0.5;
  }

  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.animated-element {
  animation: slide-in 2s ease-in-out;
}

This example defines a ‘slide-in‘ animation where an element moves from left to right while fading in. The animation is then applied to .animated-element.

Use cases:

  • UI Transitions: Create smooth transitions for UI elements like modals, dropdowns, or navigation menus.

  • Looping Animations: Design perpetual animations for loading indicators, background effects, or decorative elements.

  • Interactive Feedback: Use animations triggered by user actions to provide user feedback and enhance user experience.

  • Storytelling and Engagement: Develop complex, multi-step animations to guide users through the narrative or highlight key information on the page.

image-set()

The image-set() CSS functional notation is a powerful tool for responsive image management. It allows the browser to choose the most appropriate image from a set of options, primarily based on the device’s pixel density.

How it works: image-set() provides a list of image sources along with their resolution descriptors. The browser then selects the most suitable image based on the de’ characteristics.

.box {
  width: 400px;
  height: 200px;
  background-repeat: no-repeat;
  background-size: cover;

  background-image: image-set(
    url("https://image1.jpg") 1x,
    url("https://image2.jpg") 2x
  );

In this example, devices with standard resolution will load image1.jpg, while high-resolution devices (like Retina displays) will load image2.jpg.

Use Cases:

  • Responsive Images: Provide different image resolutions for various device pixel ratios, ensuring crisp images on high-DPI screens without unnecessarily large downloads on standard screens

  • Art Direction: Use image-set() in combination with media queries to serve different images based on both screen resolution and size, allowing for more nuanced art direction on responsive designs.

  • Performance Optimization: By serving appropriately sized images, you can significantly reduce bandwidth usage and improve load times, especially on mobile devices.

  • Future-Proofing: As new screen resolutions emerge, image-set() allows you to easily add support for these without changing your existing markup.

white-space-collapse

The white-space-collapse property offers precise control over how a whitespace is handled in text, allowing for collapsing, preserving, or other custom behaviors. This property provides more granular control than the traditional white-space property, enabling developers to fine-tune text presentation.

How it works: white-space-collapse determines how sequences of white spaces are handled within an element. It offers several values:

  • collapse: Collapses sequences of white space into a single space (default behavior)

  • preserve: Preserves all while space characters

  • preserve-breaks: Preserves line breaks but collapses other white spaces.

  • breaks-space: Similar to preserve but allows breaking at any white space character.

.collapsed-spaces {
  white-space-collapse: collapse;
}

.preserved-spaces {
  white-space-collapse: preserve;
}

.preserved-breaks {
  white-space-collapse: preserve-breaks;
}

.break-spaces {
  white-space-collapse: break-spaces;
}

These examples demonstrate different ways of handling white space within text elements.

Use Cases:

  • Code Examples: Use preserve or preserve-breaks to display code snippets with accurate spacing and indentation.

  • Preformatted Text: Maintain precise whitespace in preformatted text blocks, ensuring the layout matches the original formatting.

  • Controlling Wrapping in Long Strings: Use break-spaces to allow wrapping within long strings that lack natural word breaks, such as URLs or database keys.

  • Fine-Tuning Text Layout: Use collapse to control spacing between words and lines, creating more precise typographic adjustments.

Limit the content width in the viewport

body {
  max-width: clamp(320px, 90%, 1000px);
  /* additional recommendation */
  margin: auto;
}

Adding this one line will reduce the content size to occupy 90% of the viewport and limit its width between 320 and 1000 pixels (feel free to update the minimum and maximum values)

This change will automatically make your content look much nicer. It will no longer be a vast text block but something that looks more structured and organized. And if you also add margin: auto to the body, the content will be centered on the page. Two lines of code make the content look so much better.

Wrap headings in a more balanced way

h1, h2, h3, h4, h5, h6 {
  text-wrap: balance;
}

Headings are an essential part of the web structure, but due to their larger size and shorter content, they may look weird. Especially when they occupy more than one line. A solution that will help is balancing the headings with text-wrap.

Although balance seems to be the most popular value for text-wrap, it is not the only one. We could also use pretty, which moves an extra word to the last row if needed instead of balancing all the content. Unfortunately, pretty has yet to count onboard support.

Balanced wrapping can improve visibility and readability.

Control Element’s Height and Width

You can use the resize property to allow users to set any element’s width, height, or both manually.

There are mainly 3 values of resize. They are horizontal, vertical and both.

resize: horizontal allows you to drag the element to increase its width.

resize: vertical allows you to drag the element to increase its height.

resize: both allows you to drag the element to increase both its width and height.

But wait. It does not work for every element.

Those are — inline elements and block elements having the overflow property is set to visible .

Centering without Flexbox or Grid

Remember the struggle of centering elements with CSS?

You would reach for a flexbox or grid just to center one thing.

Not anymore!

The new align-content property makes entering elements - no extra containers.

.my-element {
  display: block;
  align-content: center;
}

✅ Supported in Chrome, Edge, and Firefox

❌ Not yet available in Safari

Variables get smarter with property

CSS variables are powerful, but the new @property at-rule takes them to the next level.

Now, you can define custom properties with specific types, inheritance rules, and default values.

Before the @property at-rule, custom properties were powerful but lacked type safety and constraints. For example:

:root {
  --rotation: 45deg;
}
div {
  transform: rotate(var(--rotation));
}

This works, but the variable is essentially unregulated.

If someone accidentally assigns an invalid value like —rotation: blue, the code breaks or behaves unpredictably.

Now, you can declare custom properties with a defined syntax, initial value, and inheritance rules.

Here’s how it works:

@property --rotation {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
div {
  transform: rotate(var(--rotation));
}
  • syntax: Specifies the type of value the property can accept. For --rotation, it’s an <angle> (like 45deg or 1turn).

  • inherits: Determines if the property should inherit its value from its parent element. In this case, we set it to false.

  • initial-value: Sets a default value if no other value is assigned. Here, it defaults to 0deg.

starting-style — Fixing animation Start Issues

  • Normally, when you hide an element (display: none), it pops up instantly when shown.

  • @starting-style defines its initial state so transitions work smoothly.

@starting-style {
  .modal {
    opacity: 0;
    transform: scale(0.8);
  }
}

.modal {
  transition: opacity 0.5s, transform 0.5s;
}

.modal.show {
  opacity: 1;
  transform: scale(1);
}

The modal fades in instead of appearing suddenly

✅ Chrome, Edge

❌ Not yet in Firefox or Safari

The math gets an upgrade

CSS has always allowed simple calculations with the calc() function.

However, its capabilities were somewhat limited.

The latest update brings a suite of new math functions, including round(), mod(), and rem().

The old way: Limited Math with calc():

.element {
  width: calc(100% - 50px);
}

The new ways with advanced math functions

.box {
  margin: round(2.5px); /* Rounds to 3px */
}
.stripe:nth-child(odd) {
  left: calc(var(--index) * 50px mod 200px);
}
.circle {
  width: rem(10px, 3px); /* Outputs 1px */
}

Forms with New Pseudo-Classes

Forms are the backbone of user interaction on the web, but ensuring users input the right data can be very tricky.

While CSS provided some basic pseudo-classes like :valid and :invalid for form validation, they weren’t always intuitive or flexible.

The new :user-valid and :user-invalid pseudo-classes refine this process by focusing on user interaction.

The old way: Static validation with :valid and :invalid

Previously, you could validate form fields with :valid and :invalid.

However, these pseudo-classes are triggered as soon as the page is loaded, leading to premature styling changes.

input:valid {
  border-color: green;
}

input:invalid {
  border-color: red;
}

The New Way with :user-valid and :user-invalid

The :user-valid and :user-invalid pseudo-classes only apply styles after the user interacts with the field, avoiding premature validation styles.

input:user-valid {
  border-color: green;
}

input:user-invalid {
  border-color: red;
}

Animating Sizes with the interpolate-size Property

Animating size changes like height, width, or padding has always been tricky in CSS.

The new interpolate-size property transforms how size animations work.

The old way: Using max-value or Javascript:

.collapsible {
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.3s ease-out;
}
.collapsible.open {
  max-height: 500px; /* Assumes a fixed maximum height */
}
const element = document.querySelector('.collapsible');
element.style.height = `${element.scrollHeight}px`;

The new way with interpolate-size

.collapsible {
  interpolate-size: height ease-in-out 0.3s;
}
.collapsible.open {
  height: auto;
}
  • The browser calculates the starting and ending sizes dynamically, even for auto values.

  • Transitions are seamless, no matter how much content is inside the element.

Remove Image Backgrounds With One Line Of CSS

There are a few images whose background does not match the website’s background, like the image below:

The background color of the image is completely different from your expected color.

The most common option you might think is to change your design.

Change the background color to the product’s background color.

Not really…

There is a property in CSS called mix-blend-mode where all the magic happens.

Your image is contained inside an individual div element. Each div has its own background color and each image has its own.

How the mix-blend-mode really work?

When you apply mix-blend-mode for the image element, the browser starts a color comparison between the image and the div element.

Here, the style will be mix-blend-mode: darken

The color comparison will be done on a pixel-by-pixel basis. In a certain position, two pixels are compared with each other.

If the mix-blend-mode property is darken, the darker pixel between the two will be kept.

Look like the image’s background has been removed, but actually it blends with the background.

Container Queries

CSS container queries introduced a new approach to responsiveness. Previously, we used media queries to create UIs that adapted to different screen sizes. But it wasn’t as easy as it sounds. There were issues in maintenance, performance, flexibility, and style overlapping.

Container queries resolve these issues by allowing developers to customize elements depending on their parent container size. Since this method doesn’t depend on the viewport size, it makes the HTML components fully modular and self-contained.

The following is a simple example of how container queries work:

.wrapper {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 20px;
}
@container (min-width: 500px) {
  .profile-card {
    grid-template-columns: 150px 1fr;
    grid-template-rows: auto 1fr;
    align-items: start;
    gap: 20px;
  }

  .profile-card header,
  .profile-card .bio {
    grid-column: 2;
  }

  .profile-card .profile-image {
    grid-row: 1 / 3;
    grid-column: 1;
  }
}

This container query adjusts the layout of the profile card when its width reaches 500px or more. It changes the card from a stacked layout (with the image on top) to a two-column layout when the image appears on the left and the text content aligns on the right.

Container queries are very useful in design systems where components need to adapt based on their immediate environment rather than the entire viewport. However, container queries still lack full browser support. If your users are using unsupported browsers or older versions, they might face styling issues.

Subgrid

Subgrid is an exciting addition to the CSS grid layout model. It allows you to inherit the grid structure of the parent grid container in child grid items. In other words, a subgrid allows you to align child elements according to the rows or columns of the parent grid. This method allows you to easily create complex nested grids without using nested grid overrides.

In the following code example, the layout uses the subgrid approach within a list:

.product-wrapper {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.product-card {
  display: grid;
  grid-template-rows: subgrid; /* Allows the nested grid to align directly with the parent grid */
}

In the example, the product-wrapper creates a flexible grid layout to control the number of columns based on the container width. Then, each product-card aligns its rows directly with the grids defined by the product-wrapper .

Pseudo-Classes

Pseudo-classes such as :hover, :focus, and :first-child are options that select HTML elements based on their state rather than their hierarchy or sequence in the document. These selectors allow developers to create more interactive and responsive UIs without using Javascript.

The following code example demonstrates several pseudo-classes in action:

// HTML
...
.hover-section:hover {
  background-color: rgb(82, 11, 145); /* Changes the background color on hover */
  color: white;
}
.input-section input[type="text"]:focus {
  border-color: orange; /* Highlights the input field when focused */
  background-color: lightyellow;
}
.list-section li:first-child {
  color: green; /* Styles the first item in a list */
}
.list-section li:last-child {
  color: red; /* Styles the last item in a list */
}

This CSS code example shows how to enhance user interaction by changing styles based on user actions, such as hovering or focusing on elements, and how to style the specific children of a container.

These pseudo-classes are pretty useful when developing forms, navigation menus, or interactive content that requires visual cues to guide user interactions.

Logical Properties

Tired of using margin-left, margin-right, margin-top, and margin-bottom?

Here’s an example that uses logical properties for layout adjustments:

.box {
  margin-block: 5px 10px;    /* 5px top margin, 10px bottom margin */
  margin-inline: 20px 30px;  /* 20px left margin, 30px right margin */
}

The same method works for padding as well.

.box {
  padding-block: 10px 20px; /* 10px top/bottom padding, 20px bottom padding */
  padding-inline: 15px 25px; /* 15px left padding, 25px right padding */
}

These properties adjust automatically based on the text direction (e.g., left-to-right or right-to-left), making your code more readable, shorter, flexible, and international-friendly.

Empty Element

No more empty space or unwanted margins from empty elements! This CSS trick automatically hides any elements that have no content. It’s incredibly useful for dynamic content that might not always be present, ensuring your content stays clean and tight.

.element:empty { display: none }

Consider combining this with pseudo-elements such as ::before or ::after for placeholders or default states when the element is empty. This can add a nice touch of detail to your design without any clutter.

Responsive Styling based on orientation

Adjust your site’s background color (or any other style) based on the design orientation with this simple media query. It’s perfect for ensuring your design looks great in both portraits or landscape modes, especially on mobile devices where users might frequently change orientation.

@media (orientation: landscape) {
  body {
    background-color: #333
  }
}

Dynamic Sibling Influence

Create an interactive element on your page without Javascript! This CSS one-liner changes the style of a target element when you hover over its sibling. It’s great for highlighting related elements or showing hidden information when another element is interacted with.

.sibling:hover ~ .target { color: #007bff }
.sibling:hover + .target { color: #007bff }

The ~ the selector in CSS targets all subsequent siblings that match the selector, allowing you to style multiple elements that follow other specific elements within the same parent.

On the other hand, the + selector targets only the directly adjacent sibling that immediately follows the specified element, limiting the effect to just one next sibling.

A practical migration handbook from SASS/SCSS to modern native CSS

Modern CSS has rapidly evolved, integrating many features that were once exclusive to Sass/SCSS. Features like CSS variables, native nesting, color functions, and cascade layers are now built into the language — eliminating the need for a preprocessor in most cases.

This article provides a practical guide for developers looking to migrate their codebase from SCSS/Sass to the modern native CSS.

The :is Pseudo Class

:is(selector1, selector2, selector3) {
  /* styles */
}

The :is pseudo-class revolutionizes the selector concept by accepting a list of selectors and styling all elements that match any of these selectors. This greatly facilitates the selection and styling of elements in the DOM.

Instead of long selector lists, you can use :is() to improve readability while avoiding a long selector.

The :has() pseudo-class

.hero:has(.hero-button) {
  background-color: var(--accent-50);
}

This CSS pseudo-class :has() provides a powerful way to select an element based on its descendants, similar to the application of conditional styles.

The :target Pseudo Class

CSS offers several pseudo-classes that let you style elements based on different states (:hover, :focus, :checked).

But there’s one you might have not used before — :target

The :target pseudo-class applies styles to an element when its ID matches the Fragment identifier in the URL (the part after #).

This behavior is commonly seen when clicking on an anchor link that jumps to a section on the same page.

Here’s a simple example:

<a href="#contact">Go to Contact</a>
<section id="contact">
  <p>Welcome to the contact section!</p>
</section>

When clicked, the URL changes to yourwebsite.com/#contact, and the section becomes the target. That’s where the power of :target comes in.

: vs:: in CSS3

Have you always used trial and error to style elements when it involves either a colon : or a pair of colon ::?

: precedes and identifies the state of an element while :: “creates” element(s). The difference between : and :: is that the former describes the state of a selected element usually involving user interaction, while the latter is used to create elements as a part of the selected element and/or used to target elements using the selected element as a reference.

It is important to note that : creates pseudo-classes; some examples are
:hover - to style an element when the user is on it without selecting ie hovers over an element
:active - to style an element when clicked ie when an element is active
:visited - to style anchor tags (links) when a user has clicked on it.
:focus - to style an input field that the user clicked on.

While :: creates pseudo-elements. Some examples of pseudo-elements are
::before - targets created element that precedes the selected element
::after - targets created element that succeeds selected e

Conditions

Conditions can be written in several ways, depending on where you want to use them. Selectors are scoped to their elements, while media queries are scoped globally and need their own selectors.

/* Attribite Selectors */
[data-attr='true'] {
    /* if */
}
[data-attr='false'] {
    /* elseif */
}
:not([data-attr]) {
    /* else */
}
/* Pseudo Classes */
:checked {
    /* if */
}
:not(:checked) {
    /* else */
}
/* Media queries */
:root {
    color: red; /* else */
}
@media (min-width > 600px) {
    :root {
        color: blue; /* if */
    }
}

Tailwind CSS

Exploring the rise of the utility-first CSS framework that’s changed the game. Here’s how Tailwind became so popular and why I have refused to use any other CSS solution since the year 2022.

From the dawn of the web, CSS (Cascading Style Sheets) has played a vital role in web development, adding the stylistic flair that separates dull, static pages from vibrant, interactive sites.

Over the years, various CSS frameworks and approaches such as Bootstrap and BEM (Block-Element-Model) have offered developers convenience and speed. However, there is a new reigning champion: TailwindCSS. Since its initial release in 2017, Tailwind has gained massive popularity among developers. But what sets Tailwind CSS apart from other approaches?

Flexibility Reigns Supreme: Tailwind > Bootstrap

When using conventional CSS frameworks like Bootstrap or component libraries like Material UI, components come predefined with their style. This feature, while convenient, is incredibly restrictive, especially when developers are working with professional designers who want a ton of customization, responsiveness, and interactivity in their apps and sites.

Tailwind CSS introduced a fresh paradigm: utility-first. Instead of wrestling with overbearing default styles, we now start with barebone HTML (or JSX) and build our designs from the ground up, applying classes that directly correspond to CSS properties but are much shorter to type out—especially for media queries. Initially, this granular control feels weird, but it affords developers unparalleled flexibility.

Customizing predefined components to fit a unique design can be an enormous struggle. In my experience, unless the designer is already comfortable with the developer's prebuilt approach, the developer will not save any time using prebuilt components.

Even worse is the feeling of trying to implement a complete design system using a coarse tool like Material UI. It seems great at first, but once you get past the prototyping stage, you will start having theming nightmares.

To address this issue, Tailwind CSS provides a highly customizable system. With the help of the configuration file, tailwind.config.js, developers can adjust the framework’s default theme, and define custom colors, spacings, font sizes, and much more. The level of customization facilitates the creation of unique designs without straying away from the framework’s philosophy.

More importantly, Tailwind CSS provides a pre-built template for a design system. Do you want a darker pink? Forget about hex codes; you’re just going to switch Barbie bg-pink-400 to a slightly darker bg-pink-500. It’s that easy.

Uncovering the Origin Story of Tailwind CSS

Tailwind CSS was born from the minds of Adam Wathan and Steve Schoger. Adam Wathan is a full-stack developer known for his “Refactoring to Collections“ book and numerous video courses on web best practices. He had previously been a fan of Bootstrap and even created a course on it. However, over time, he found himself frustrated with the rigidity of pre-designed components and shared his sentiments openly on Twitter.

Following a series of tweets critiquing the traditional CSS framework model, Wathan announced in October 2017 that he was building his own CSS framework, initially named “Functional CSS“. This new framework didn’t take long to generate buzz in the online developer community.

The framework’s unique selling point is its utility-first philosophy, which gives developers maximum control over styling while eliminating the need to write custom CSS for individual components. In contrast of traditional frameworks, which often required overriding predefined styles, this utility-first approach provided a granular level of control right out of the box.

The Transformation: Functional CSS to Tailwind CSS

As work on the project progressed, it became apparent that the term “Functional CSS“ was too generic for such a unique framework and approach. Thus, in early 2018, Wathan rebranded it as “Tailwind CSS“. The name was a fitting metaphor for the mission: to create a tool to help professional developers sail smoothly through the choppy seas of CSS.

Since its branding and official launch, Tailwind CSS has grown exponentially in popularity. The framework's adoption has been aided by its developer-focused design and an active community of developers who regularly contribute to its ecosystem. The 2020 release of Tailwind UI was a significant milestone, providing a vast library of ready-made components and further enhancing the framework’s appeal.

PurgeCSS: A Lean and Mean Approach To Efficiency

Many developers might raise their eyebrows at the prospect of a utility-first CSS framework, given the potential for file size bloat. The idea here is that if you load “every possible Tailwind CSS“ class, you will end up with a particularly large CSS file. This concern, while valid, is obliviated by how Tailwind uses PurgeCSS automatically, with no configuration required.

PurgeCSS significantly reduces the size of the final CSS file by removing unused classes from the stylesheet, thus ensuring that the production build is as lean as possible. Because of PurgeCSS, Tailwind’s utility-first approach doesn’t inflate the file size, making it a viable option for performance-conscious projects. It should be your default option if you care about web performance (and core web vitals).

To justify that claim, let’s very briefly touch on the alternatives. If you are using CSS-in-JS, you will bloat every single component and have zero reusability, so let’s say you end up with 100kb of data transfer. The BEM model will be similar since you will type display: flex; into several dozen separate components, resulting in ~100kB data transfer.

You could theoretically optimize a regular CSS file, with or without PurgeCSS, but you’ve never seen anyone do it. Typical professional-grade CSS files tend to be 10,000 lines or more, and you generally can’t remove anything to prevent breaking things, at least not without a tool like PurgeCSS. In comparison, TailwindCSS will only send the required CSS, which usually results in just 20-40 kB of data transfer for a big website.

The Theoretical Performance Bonus of Tailwind CSS

I’m happy to provide a quick theoretical basis for why Tailwind CSS results in less data transfer than traditional CSS files or CSS modules.

Imagine you have 100 components that use Flexbox to center content. You are using BEM or CSS modules, so you have to write the following.

.block_element_modifier_name {
  display: flex;
  justify-content: center;
  align-items: center;
}

Then, you consume that piece of CSS in HTML or a JS Framework-Powered component, like my preferred choice, React JSX.

<div className="block_element_modifier_name">
  The content
</div>

The result is 5 lines of CSS (95 characters) and 3 lines of JSX (64 characters). Multiply those by 100x, and you have 9500 + 6400 = 15900 characters.

Now compare that to Tailwind CSS, which automatically generates this CSS when you use the flex, justify-center, and items-center utility classes:

.flex {
  display: flex;
}

.justify-center {
  justify-content: center;
}

.items-center {
  align-items: center;
}

Now we have 11 lines of CSS (104 characters), with any unused CSS classes automatically purged via PurgeCSS - something you don’t get with BEM.

Tailwind CSS is incredibly performance-focused and aims to produce the smallest CSS file possible by only generating the CSS you are actually using on your projects

Tailwind Docs

Continuing with our Tailwind CSS example, we would consume those built-in utility classes in our HTML (or again, in our React JSX file) as follows:

<div className="flex items-center justify-center">
  The content
</div>

We still have 3 lines of JSX (69 characters) that we have to repeat 100 times in our code base. But our CSS doesn’t get multiplied by 100x, just 1x.

The result is 104 characters of CSS + 6900 characters of JSX, or 7004 characters (bytes) total. That’s less than half of the previous 15900 bytes!

Of course, modification and network compression will adjust these results downward somewhat, but BEM is not really as compressible as Tailwind.

Why not? Well, Tailwind rhymes — "flex items-center justify-center" will repeat in your codebase, but each .block_element_modifier_name is unique.

if we take the difference of 15900 minus 7004 bytes, Tailwind has an advantage of 8896 bytes (or about 9kB, allowing for rounding errors).

When we multiply that by another 10x component to allow for typical complexity, Tailwind has a 90kB data transfer size advantage vs BEM.

Combined with minification and network compression, Tailwind CSS usually leads to CSS files that are less than 10kB, even for large projects.

For example, Netflix uses Tailwind for Netflix Top 10 and the entire website delivers only 6.5kB of CSS over the network — ibid

The Developer Experience is Intuitive and Simple

The rise of Tailwind CSS cannot solely be attributed to its technical features. The overall developer experience it provides is a big factor in its popularity. Once I got used to it, I found Tailwind intuitive. Putting the class right in the HTML feels normal, and it requires less typing than CSS. Plus, there’s Tailwind CSS Intellisense and a Tailwind Prettier extension.

What do I mean when I say one of the key features of Tailwind CSS is its intuitive nature? Well, let’s take the display: flex; example. In Tailwind, that becomes flex. Easy, consistent, memorable, and short class names are intuitive, at least after the initial learning curve. The whole point is to use human-readable class names that map closely to their CSS property counterparts. This attribute makes Tailwind CSS relatively easy to pick up, particularly for those with existing knowledge of how CSS works.

Additionally, I find the mental load so much simpler than when I used regular style sheets (whether CSS or SASS). We all know that inline styles prevent reusability. Compared to a regular CSS file with reusable classes, inline styles cause code bloat. They make your code hard to read and extremely difficult to maintain since your codebase is not DRY.

Switching back and forth all day to a CSS file is a bug burden, especially when trying to find each media query for three or four different screen sizes. Something like BEM helps reduce the chance of accidentally changing something you didn’t mean to, but you are still splitting your time looking at HTML in one file and CSS in another. I find Tailwind CSS much faster and easier to write because it all goes in the same file as the HTML!

Tailwind’s Thriving Community and Resources

Tailwind CSS boats a thriving community that contributes to its growth. The availability of tutorials, resources, and plugins built around Tailwind makes learning and using the framework more accessible. A great example is the Line Camp Plugin, which automatically shortens lines using an ellipsis (). That plugin was so popular that it’s now built into Tailwind.

Additionally, the Tailwind development team frequently communicates with the community, making updates and refining the product based on user feedback. The other example I should mention is that you can now use arbitrary styles, such as w-[1.2rem]instead of only built-in classes (w-4) or those that you’ve added to the theme via the tailwind.config.js file. (w-button?). That’s “Just In Time” in action.

Tailwind UI: Here are Your Prebuilt Components

Tailwind UI, a commercially available component library built with Tailwind CSS, has also contributed to the framework’s success. It offers easy-to-customize prebuilt components that are beautifully designed, fully responsive, and carefully crafted. While I find the Tailwind UI components to be a little poorly documented—they usually lack comments—they are super handy as a starting point for front-end development.

Tailwind UI seamlessly integrates with existing Tailwind CSS projects, whether they’re greenfield (brand-new) or much further along. If you are stuck, reaching for a Tailwind UI example component is a perfect fit for developers looking to expedite the building process without sacrificing quality. Although it’s a paid resource, it’s a pretty huge library that gets frequent updates, so I have found it’s a good investment.

When I need to build a complex web application from scratch, I use the open-source version of Tailwind UI called Headless UI. Headless UI provides fully accessible, unstyled UI components for many common frontend patterns, such as Listboxes (Dropdowns) or Dialogs (Modals). Additionally, the examples of each component are styled with—you guessed it—Tailwind CSS, making them easy to add to a project.

Best Practices for Tailwind CSS

Do not repeat the same class names for elements

Prefer to create separate components with uniform styles throughout the application instead of writing the same class names in multiple places or creating a new class name including all the tailwind class names using @apply directive.

Doing so would offer us the flexibility of updating our styles with minimal effort, as we have to update the class names in only a single place.

Suppose we have a h1 tag that will be used in multiple places. We have two approaches to achieve it:

  1. We can create a component H1Heading with tailwind classes applied to it and reuse this component whenever it is required.

     const H1Heading = ({ children }) => {
       return (
         <h1 className="text-2xl md:text-4xl lg:text-5xl xl:text-6xl font-bold text-black">
           {children}
         </h1>
       );
     };
    
  2. We can create a class in our CSS file that includes all the tailwind class names for the particular element and use the class name wherever required.

     @tailwind base;
     @tailwind components;
     @tailwind utilities;
    
     .heading-1 {
         @apply text-2xl md:text-4xl lg:text-5xl xl:text-6xl font-bold text-black
     }
    
     <h1 className="heading-1">
       Some random heading
     <h1>
    

Maintain an organized style guide using design tokens

Design tokens are a way to store and manage your design variables, such as color palettes, spacing scale, typography scale, or breakpoints. With design tokens, you can create consistent and reusable styles that are easy to update and maintain.

You can create the design tokens in the tailwind.config.js file. This is a good way to centralize your design tokens and make them available to all of your Tailwind CSS classes.

Suppose our application follows a particular design system. We can add these guidelines to our Tailwind configuration:

import defaultTheme from "tailwindcss/defaultTheme";

const config = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      fontFamily: {
        manrope: "var(--font-manrope)",
      },
      colors: {
        ...defaultTheme.colors,
        theme: "#7743DB",
        light: {
          primary: "#FFFFFF",
          secondary: "#f1f1ef",
        },
        dark: {
          primary: "#0F0F0F",
          secondary: "#202020",
        },
        "background": "#F5F5F5",
      },
      screens: {
        ...defaultTheme.screens,
        xs: "340px",
        sm: "420px",
      },
      spacing: {
        spacing: {
          ...defaultTheme.spacing,
          1: '5px',
          2: '10px',
          3: '15px',
          4: '20px',
          5: '25px'
        }
      }
    },
  },
};

Avoid using arbitrary values

Imagine our web application follows some color scheme where we have #7743DB as our theme color and #0D0D0D as our background color.

We can add these colors to our tailwind configuration and refer to them using class names, such as bg-background text-theme instead of using arbitrary values at multiple places, i.e. bg-[#0D0D0D] or text-[#7743DB].

Now, if we want to change our application’s color scheme, we just need to update our tailwind configuration instead of renaming the arbitrary class names in multiple places.

import defaultTheme from "tailwindcss/defaultTheme";

const config = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        ...defaultTheme.colors,
        theme: "#7743DB",
        background: "#0D0D0D"
      },
    },
  },
};

export default config;
const ColoredText = ({ children }) => {
  return (
     <span className="text-theme">
          {children}
     </span>
  );
};

Avoid applying dynamically generated class names

You might have encountered this issue while working with dynamic classes: Whenever we apply dynamic class names based on a state or condition, the class name appears in the browser's elements panel, but its corresponding CSS does not.

const BorderBox = ({ borderColor }) => {
  return (
    <div className={`border border-solid border-[${borderColor}]`}>
      Some Random Text
    </div>
  );
};

This is because Tailwind scans your source code for classes using regular expressions to extract every string that could possibly be a class name. Hence, any broken class name string such as border-[${borderColor}] would not be recognized by Tailwind at build time, and it would not be included in the output CSS file of Tailwind.

Suppose we have to change the border color of our element based on the color code passed in props. There are two ways to it:

  1. Defining a separate class name for each state value. This is only applicable if you know all the expected values of borderColor at the build time.

     const BorderBox = ({ borderColor }) => {
       return (
         <div
           className={clsx("border border-solid", {
             "border-black": borderColor === "black",
             "border-red-500": borderColor === "red",
             "border-green-500": borderColor === "green",
           })}
         >
           Some Random Text
         </div>
       );
     };
     // Note: clsx is utility package for constructing className strings conditionally.
    
  2. If we do not know all the expected values of borderColor at the build time, it is better to pass the border color in the style attribute of the element to support unknown values.

     const BorderBox = ({ borderColor }) => {
       return (
         <div className="border border-solid" style={{ borderColor }}>
           Some Random Text
         </div>
       );
     };
    

Creating a utility to read Tailwind Configuration in Javascript

In web applications, a situation rarely arises where you need to read some CSS design token value in Javascript, but when it does, we generally hardcode the CSS design token value in our code while working with Tailwind. This is not a good practice as in the future, if you change your design token value in your Tailwind configuration, your code might still refer to the old value, which can cause unwanted behavior.

Hence, we can build a custom utility function to read the Tailwind configuration in our code.

import resolveConfig from "tailwindcss/resolveConfig";
import tailwindConfig from "../tailwind.config";

const getTailwindConfiguration = () => {
  return resolveConfig(tailwindConfig).theme;
};

const config = getTailwindConfiguration();

console.log(config.colors.red[500]); // would print #ef4444

Defining Tailwind Plugins to register new/complex CSS styles

Tailwind provides us with plugins to register new styles and inject them into the user’s stylesheet using Javascript instead of writing custom CSS styling in stylesheets.

I prefer this approach, as writing custom CSS classes means essentially rewriting CSS and sacrificing Tailwind’s organized workflow and simple maintenance.

Suppose we want to create a .btn class that has several styles attached to it. This is how we can achieve it:

import plugin from "tailwindcss/plugin";

const config = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  plugins: [
    plugin(function ({ addComponents }) {
      addComponents({
        ".btn": {
          padding: ".5rem 1rem",
          borderRadius: ".25rem",
          fontWeight: "600",
        },
      });
    }),
  ],
};

export default config;

This is how the .btn class would look like when you hover over it:

Tailwind supports registering complex styles as well, along with user-provided value. You can go through the Tailwind official documentation for the plugin to know more about it.

Tailwind CSS Tips & Tricks

Styling Based on Other Elements

Tailwind CSS has some features that allow you to style an element based on another element.

  1. Styling children from parent element (`&_*`)

Generally, you can just put utility classes to any element to style it. But, there is a case when you don’t have control over certain elements, like when you are using external components.

In that case, you can use this class to style elements inside those components. There are 2 ways to style elements inside its parent:

  • Using *:{utility-class} to style direct children, which is documented here.

  • Using [&_{children-selector}]:{utility-class} to style any descendant for the parent element. I don’t know why, but this way is not documented in official documentation.

<div class='[&>p]:font-bold *:uppercase [&_.link-class]:text-teal-500'>
    <p>
        This text is styled by{' '}
        <a class='link-class' href='#'>
            its ancestor
        </a>
    </p>
</div>

The underscore in the code is the replacement for space. You can also use id or class selectors.

  1. Style based on Parent state (group-*)

Let’s say you have a card component that has text, a link, and an icon inside it. When the user hovers on the card, you want to apply a style to the text, link, and icon. To do that, you can put group class to the card element and put group-hover:{utility-class} to the text, link, and icon that you want to style.

The group state is not limited to hovering. You can also apply it for other states like active, focus, and checked.

<a
    href='#'
    class='group block max-w-xs mx-auto rounded-lg p-6 bg-white ring-1 ring-slate-900/5 shadow-lg space-y-3 hover:bg-sky-500 hover:ring-sky-500 hover:no-underline'>
    <div class='flex items-center space-x-3'>
        <svg
            class='h-6 w-6 stroke-sky-500 group-hover:stroke-white'
            fill='none'
            viewBox='0 0 24 24'
            stroke='currentColor'
            stroke-width='1.5'
            stroke-linecap='round'
            stroke-linejoin='round'>
            ...
        </svg>
        <h3 class='text-slate-500 group-hover:text-white text-base'>New project</h3>
    </div>
    <p class='text-slate-500 group-hover:text-white'>Create a new project from a variety of starting templates.</p>
</a>

In the code above, you can see there are group-hover:text-white inside the element with group class

  1. Style based on sibling state peer-*

You can use peer-{state}:{utility-class} to style an element based on its sibling state.

<form class='max-w-sm mx-auto'>
    <label class='block'>
        <span class='block text-sm font-medium text-slate-700'>Email</span>
        <input
            type='email'
            class='peer rounded p-3 w-full outline-none border border-solid border-slate-300'
            placeholder='Type an invalid email to see error message'
        />
        <p class='mt-2 invisible peer-invalid:visible text-pink-600 text-sm'>Please provide a valid email address.</p>
    </label>
</form>

In the demo above, you can see that the error message is only visible when the input element (sibling) has invalid state.

  1. Style based on descendant has-*

There is also a way to style an element based on its children or descendants. The example use case for this is when looping a post list that the post item can contain an image or not.

In real use cases, you probably use javascript logic instead using has-[{element-selector}] to style based on the descendant element. But, has-* also supports for descendant state.

In the demo above, you can see that I used has-[img:hover] to change the background when the image hovered. You can use another state like invalid, disabled, focus, etc. This is the best case where you can use has-* selector.

You can also combine has-* with another modifier like peer-has-*.

Writing and Customizing Classes

Tailwind CSS supports some ways to write and customize utility classes.

  1. Custom Classes with Arbitrary Values

Custom classes with Arbitrary Classes are like writing CSS directly inside the class. It looks something like text-[#09836d] or mr-[75px]. You can use a class with arbitrary as the last resort when you can’t find any predefined utility class that fulfills your use case.

You can pass any value that is valid, such as the CSS property class, to a custom class. You can even pass Tailwind CSS variables. You can find more examples of Arbitrary values in the official documentation.

One thing that usually becomes an issue when using classes with arbitrary values is when you need to pass a value with a space. For example, when you want to pass box-shadow value 0 0 2px 0 #abcdef. In this case, you can replace the space with _, so the class will be shadow-[0_0_2px_0_#abcdef].

Keep in mind that a CSS class can’t contain whitespace.

  1. Color Opacity

Did you know that you can apply opacity to your color classes? It works in classes that contain color value like bg-{color}, text-{color}, border-{color}. You just need to add /{opacity-value} after the class. The opacity value is 0 to 100 with an interval of 5.

  1. Customize / Disable Tailwind container class

Tailwind CSS comes with a built-in responsive container class. The container will adjust in xs, sm, md, and lg breakpoints. By default, the container class has no padding and center positioning. But if you want to customize it, you can add container configurations in tailwind.config.js file.

 /** @type {import('tailwindcss').Config} */
module.exports = {
 ...,
  theme: {
    container: {
      center: true,
      padding: '1rem'
 },
 },
}

You can also disable or remove the container class by setting container: false in corePlugins configuration.

/** @type {import('tailwindcss').Config} */
module.exports = {
 ...,
  corePlugins: {
    container: false
 },
}
  1. Space between elements (space-* and gap-*)

When displaying a list or grid, we usually need spacing between list or grid items. We often use padding-{top/bottom/left/right} or margin-{top/bottom/left/right} and end up with excess spacing in the last item.

You can solve this by using space-* or gap-* in the list or grid container. space works for all element displays (block, flex, grid, etc), while gap-* only works for flex and grid element.

If you use space-*, you have to define the direction, whether vertical or horizontal spacing. Use space-x-* for horizontal space and space-y-* for vertical spacing.

  1. Replace Js substring() with truncate and line-clamp-*

Before I knew truncate and line-clamp-*, I usually used the javascript function substring(0, n) to truncate a string. However, the sentences often have inconsistent widths because each letter has a different width.

truncate and line-clamp-* solve this, because they don’t care about the number of letters, they only care about the number of lines.

You can use truncate to truncate sentences into 1 line. If you want to truncate a paragraph after, let’s say 3 lines, you can use line-clamp-3.

<div class='max-w-xs mx-auto bg-slate-100 rounded p-3 mb-3'>
    <p class='truncate'>
        This is text with `truncate`: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur
        adipiscing elit.
    </p>
</div>
<div class='max-w-xs mx-auto bg-slate-100 rounded p-3'>
    <p class='line-clamp-2'>
        This is text with `line-clamp-2`: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur
        adipiscing elit.
    </p>
</div>

CSS and “vibe coding“. Why good front end dev still matter in the age of AI

AI coding tools — from large language models like ChatGPT to code completion assistants like GitHub Copilot or Cursor - are becoming daily part of front end development. As many developers are asking themselves: “Can’t we just throw some Tailwind CSS classes at it and let the AI handle the rest?

It’s true: utility-first frameworks like Tailwind CSS are generally well-supported by AI. But, when it comes to classic CSS, most models struggle. Autocomplete stutters, layout suggestion fall flat, and debugging support is often hit or miss.

So Why is CSS still so hard for AI? — and why does it mean that good frontend developers, who actually understand how CSS works, are more important then ever? Let’s break it down.

Why AI loves Tailwind, but struggles with CSS

Tailwind is consistent, predictable, and tokenized

Tailwind follows a strict naming pattern: bg-blue-500, text-white, p-4, flex, items-center. Each class maps directly to a single CSS rule, and the name conversation is easy to learn — for humans and machines alike.

This consistency is exactly when the large model language thrive on. When AI sees bg-, it knows the color is coming. Since Tailwind classes appear thousands of times across training datasets (in tutorials, GitHub repos, StackOverflow answers), AI tools get really good at recognizing and generating them.

Traditional CSS is… chaotic (in the eyes of AI)

With classic CSS, anything goes:

  • One dev uses .btn-primary, another uses .buttonMain.

  • One project uses BEM (.block__element--modifier), another uses scoped styles or CSS Modules.

  • Styles can live in external files, embedded <style> tags, or inline.

This freedom is great for humans, but terrible for pattern-based models. There is no consistent naming conversation. And the relationship between styles and HTML is often indirect. AI can’t see the result of your design choices — it only guesses based on the structure and naming, which varies wildly from project to project.

CSS is a language with depth

CSS is not just syntax — it’s a layout system with inheritance, specificity, media queries, fallbacks and quirks. It interacts with the DOM and the browser rendering engines. Understanding why something is broken often require reasoning across several layers of abstraction:

  • Is the z-index failing because of stacking context?

  • Is the margin collapse due to blocking context?

  • Is this width being overridden by a media query or a min-width somewhere else?

LLMs don’t “understand” visual context or cascading logic the way humans do. They just complete text based on what they have seen before.

That makes them decent at producing short, utility-style snippets — but poor at deep CSS reasoning.

LLMs don’t know what’s new — and that’s a problem

Let’s be real: ChatGPT doesn’t know your CSS future.
Most LLMs, even the best ones, are trained on datasets that are months — sometimes years — out of date.
So when you ask it about :has(), container-type, or the view-timeline property… it might blink. Or worse: hallucinate.

It will give you answers like:

Try using @container for responsive layouts,”
without knowing your browser targets.
Without checking
Can I use.
Without telling you that Chromium supports it but Firefox
just started experimenting*.*

In short: It doesn’t know what it doesn’t know. And that’s dangerous.

New CSS is moving fast.

  • Logical properties are changing how we handle RTL layouts

  • Container queries are rewriting responsive design

  • Scroll-driven animations are making scroll-behavior: smooth look ancient

If you rely on AI alone, you’re building with stale tools.
And in frontend, stale breaks fast.
So: trust the spec. Check the browser support. Follow CSS Working Group drafts. Stay sharp.

Because ChatGPT isn’t reading the MDN changelog every morning.
You are. Or you should.

Why CSS knowledge still matters (a lot)

AI can’t replace conceptual thinking

LLMs are great at pattern completion, but week at actual design reasoning. They can’t see how your layout looks, how your components align, how readable your typography is. They might know the syntax for a flex container, but they don’t understand when and why to do it:

So when it comes to:

  • fixing obscure layout bugs,

  • adjusting a design for subtle visual balance,

  • or building a complex component that adapts perfectly across breakpoints,

You still need someone who understand what’s happening under the hood.

Tailwind only works if you understand CSS

Tailwind is CSS — just abstracted. If you don’t understand what flex, justify-between, or overflow-hidden actually do, you’re flying blind. AI can suggest Tailwind class combos all day, but if you don’t understand the impact, you are just stacking utilities and hoping for the best.

To use Tailwind effectively, you need to understand:

  • spacing systems,

  • responsive breakpoints,

  • positioning and display rules,

  • and how to debug when things go wrong.

Tailwind doesn’t eliminate CSS — it just demands that you think in smaller, composable CSS pieces. Without the mental model of CSS, you can’t reason about why your utility stack behaves the way it does.

Real-world layout problems still require humans

When things get weird — a hidden scrollbar, a layout shift on mobile, an unexpected min-width causing an overflow – AI usually fails to diagnose the issue correctly.

This is where human developers shine. We can:

  • inspect the cascade,

  • check for box-sizing issues,

  • identify unintended inheritance,

  • and debug responsive breakpoints across real devices.

The AI just sees code. You see the result.

\=> That’s why AI can’t replace developers — not just because it lacks visual understanding, but because it can’t feel what you feel when something just looks off.

References

https://dev.to/madza/18-github-repositories-to-become-a-css-master-lab

https://defensivecss.dev/

https://moderncss.dev/12-modern-css-one-line-upgrades/?ref=dailydev

https://dev.to/poetryofcode/5-hidden-css-properties-you-didnt-know-existed-12h8

https://dev.to/alvaromontoro/css-one-liners-to-improve-almost-every-project-18m?ref=dailydev

https://medium.com/design-bootcamp/7-new-css-features-you-dont-know-about-226dd2921cb4

https://medium.com/design-bootcamp/remove-image-backgrounds-with-one-line-of-css-181581eb05fc

https://medium.com/syncfusion/5-modern-css-styles-you-should-know-in-2024-3c2bc3ec6093

https://blog.stackademic.com/the-top-6-css-cheatsheets-that-will-save-you-hours-2e1d29ed5c24

https://matemarschalko.medium.com/20-css-one-liners-every-css-expert-needs-to-know-bef568ddc265

https://medium.com/@karstenbiedermann/goodbye-sass-welcome-back-native-css-b3beb096d2b4

https://kaderbiral26.medium.com/15-useful-css-properties-you-should-know-about-d924343d6f9c

https://dev.to/stephikebudu/-vs-in-css3-13le?ref=dailydev

https://medium.com/better-programming/why-tailwind-css-became-so-popular-a-developers-guide-11213c08fa46

https://medium.com/coding-at-dawn/why-tailwind-css-is-48-better-for-performance-than-css-in-js-93c3f9fd59b1

https://medium.com/@imanshurathore/best-practises-for-tailwind-css-in-react-ae2f5e083980

https://devaradise.com/tailwind-css-tips-tricks/

https://pradipkaity.medium.com/css-only-click-handlers-you-might-not-be-using-but-you-should-69b6c9a07bf2

https://medium.com/developers-corner/these-css-pro-tips-and-tricks-will-blow-your-mind-8b58a4a682e5

https://medium.com/@karstenbiedermann/css-and-vibe-coding-why-good-frontend-devs-still-matter-in-the-age-of-ai-09797a7f1287

https://medium.com/@karstenbiedermann/the-10-best-new-css-features-in-2025-already-supported-in-all-major-browsers-c4a4cbbf71ea

0
Subscribe to my newsletter

Read articles from Tuan Tran Van directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tuan Tran Van
Tuan Tran Van

I am a developer creating open-source projects and writing about web development, side projects, and productivity.