Making your website mobile-first with Tailwind CSS

Learn how to implement mobile-first design using Tailwind CSS, a popular utility-first CSS framework. With mobile-first design, you prioritize the layout and functionality of your website or application for smaller screens and then scale up for larger screens. This approach can lead to a more responsive and accessible user experience.

2023-04-05

Written by: Zowie

tailwindcss icon

mobile

response

design

flex

media queries

Making your website mobile-first with Tailwind CSS

If you want to make your website mobile-friendly, Tailwind CSS is a great tool to use. It scans all of your HTML files, JavaScript components, and any other templates for class names, generating the corresponding styles and then writing them to a static CSS file. The best part is that it’s fast, flexible, and reliable with zero-runtime.

Steps to get started

Utility-First Fundamentals

When styling elements on the web, the traditional approach is to write CSS. However, with Tailwind CSS, you can style elements by applying pre-existing classes directly in your HTML.

But why not just use inline styles?

Some may wonder if this approach is the same as using inline styles, and in some ways, it is. Both approaches apply styles directly to elements instead of assigning them a class name and then styling that class.

However, using utility classes has several important advantages over inline styles:

By using utility classes, you can take advantage of a predefined set of styles to quickly and easily build visually consistent and responsive user interfaces. When starting to use Tailwind CSS, it’s important to understand the relationship between utility classes and common CSS. Be sure to check the documentation for more information.

Responsive Design

Using responsive utility variants to build adaptive user interfaces.

Every utility class in Tailwind can be applied conditionally at different breakpoints, which makes it a piece of cake to build complex responsive interfaces without ever leaving your HTML.

To add a utility but only have it take effect at a certain breakpoint, all you need to do is prefix the utility with the breakpoint name, followed by the : character:

<!-- Width of 16 by default, 32 on medium screens, and 48 on large screens -->
<img class="w-16 md:w-32 lg:w-48" src="..." />

This works for every utility class in the framework, which means you can change literally anything at a given breakpoint — even things like letter spacing or cursor styles.

Working mobile-first

By default, Tailwind uses a mobile-first breakpoint system, similar to what you might be used to in other frameworks like Bootstrap.

What this means is that unprefixed utilities (like uppercase) take effect on all screen sizes, while prefixed utilities (like md:uppercase) only take effect at the specified breakpoint and above.

Targeting a breakpoint range

By default, styles applied by rules like md:flex will apply at that breakpoint and stay applied at larger breakpoints.

If you’d like to apply a utility only when a specific breakpoint range is active, stack a responsive modifier like md with a max-* modifier to limit that style to a specific range:

<div class="md:max-xl:flex">
  <!-- ... -->
</div>

If you need to use a one-off breakpoint that doesn’t make sense to include in your theme, use the min or max modifiers to generate a custom breakpoint on the fly using any arbitrary value.

<div class="min-[320px]:text-center max-[600px]:bg-sky-300">
  <!-- ... -->
</div>

Handling Hover, Focus, and Other States

Every utility class in Tailwind can be applied conditionally by adding a modifier to the beginning of the class name that describes the condition you want to target.

For instance, you can style elements on hover, focus, and active states using the hover, focus, and active modifiers as shown below:

<button
  class="bg-violet-500 hover:bg-violet-600 active:bg-violet-700 focus:outline-none focus:ring focus:ring-violet-300 ..."
>
  Save changes
</button>

Tailwind offers modifiers for almost everything you need, including:

You can also stack these modifiers to target more specific situations, such as changing the background color in dark mode, at the medium breakpoint, on hover:

<button class="dark:md:hover:bg-fuchsia-600 ...">Save changes</button>

By taking advantage of Tailwind’s extensive set of modifiers, you can easily and precisely target any element state or condition, making it a powerful tool for creating dynamic and responsive interfaces.

Dark Mode

strategy

When implementing a dark mode, there are two common strategies: using a CSS class (class) or using CSS media queries (media).

Using a CSS class involves defining a class, for example, .dark-mode, which contains the styles that should apply when the dark mode is active. Then, in the HTML markup, you can add or remove this class to switch between the light and dark modes. This approach is straightforward and flexible, as it allows you to toggle the dark mode on and off easily, and you can customize the styles for the dark mode as needed.

On the other hand, using CSS media queries involves defining the styles for the light and dark modes in separate blocks of CSS code and then applying them based on the user’s system preferences or settings. This approach relies on the user’s browser to detect the preferred color scheme and apply the appropriate styles. This approach can simplify the implementation, as there is no need to add or remove a class to switch between modes. However, it can be less flexible as the styles are applied automatically based on the user’s preferences.

In summary, both approaches have their advantages and disadvantages, and the choice depends on your specific needs and preferences.

Regardless of which strategy you choose, you can use window.matchMedia to check if a given media query matches the current viewport. This method is commonly used in responsive web design to apply different styles to elements based on the viewport size, orientation, and other device properties. When used in conjunction with addEventListener, it can also be used to detect changes in the viewport and update the styles accordingly.

const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');

const isDarkMode = darkModeQuery.matches;

darkModeQuery.addListener((e) => {
  if (e.matches) {
    // detect switch to dark mode
  } else {
    // detect switch to light mode
  }
});

apply in astro

Astro has a feature to add inline scripts directly to your Astro files. These scripts will run as soon as the HTML is loaded, preventing the flicker of inaccurate color theme, which is a very common problem when implementing dark mode with hydration. You can read more about inline scripts in the Astro documentation.

The following code retrieves the user’s preferred theme and adds it to the HTML element. Feel free to copy/paste or modify this code snippet into your Astro project.

<head>
  <script is:inline>
    try {
      if (
        localStorage.theme === 'dark' ||
        (!('theme' in localStorage) &&
          window.matchMedia('(prefers-color-scheme: dark)').matches)
      ) {
        document?.documentElement.classList.add('dark');
        document
          ?.querySelector('meta[name="theme-color"]')
          ?.setAttribute('content', 'red');
      } else {
        document.documentElement.classList.remove('dark');
      }
    } catch (_) {}
  </script>
  <!-- other tag -->
</head>

First, you must ensure that the first thing that the browser does when rendering your website is to check in which mode to render. This is achieved by running JavaScript code immediately in the <head> tag of your page.

However, that is not always enough if your website is built using Astro. By default, Astro will optimize and bundle all JavaScript code that it finds within <script> tags in your .astro files. This is usually really nice but not if the code is checking for dark/light mode - this will create a flicker on each page load, as the script is put in an external script file by Astro.

To prevent Astro from optimizing your script tag, your must use the is:inline directive. This tells Astro to leave your script tag as it is and not bundle it in an external file.

Toggle Color Mode Case

Other Solution

If you don’t want to worry about specifying the color for each individual tag, there is another solution that you can try (you can even input one color to customize a website’s theme based on your color). Check out this article for more information on how to implement it.

Reusing Styles

If you don’t want the following to happen in your code, let’s check out

<div class="mt-3 flex -space-x-2 overflow-hidden">
  <img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" />
  <img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" />
  <img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" />
</div>

Using editor and language features

I highly recommend using the multi-cursor editing feature in VSCode, which is convenient for renaming and handling multiple inputs. You can also use mapping to generate uppercase text and avoid unnecessary input. In some languages or frameworks, you can use template-like methods to resolve this problem.

Compared to CSS abstractions

Don’t rely on CSS classes to extract complex components. Instead, create a template partial or JavaScript component. When you create components and template partials, there’s no reason to use anything other than utility classes because you already have a single source of truth for the styles.

Extracting classes with @apply

Use the @layer directive to tell Tailwind which “bucket” a set of custom styles belong to. Valid layers are base, components, and utilities.

<!-- Before extracting a custom class -->
<button
  class="py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
>
  Save changes
</button>

<!-- After extracting a custom class -->
<button class="btn-primary">Save changes</button>

<style>
  @layer components {
    .btn-primary {
      @apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
    }
  }
</style>

Learn more about @apply and @layer in the Functions & Directives documentation.

Adding Custom Styles (CORE-CONCEPT)

So far, I’ve only learned a little bit about Tailwind, but I’ve found that it allows you to write less code. By using Tailwind’s utility classes, you can quickly and easily style your HTML elements without having to write custom CSS from scratch.

Customizing your theme

If you want to change things like your color palette, spacing scale, typography scale, or breakpoints, add your customizations to the theme section of your tailwind.config.js file.

Using arbitrary values

When you find yourself really needing something like top: 117px to get a background image in just the right spot, use Tailwind’s square bracket notation to generate a class on the fly with any arbitrary value:

<div class="bg-[#bada55] text-[22px] before:content-['Festivus']">
  <!-- ... -->
</div>

<div class="grid grid-cols-[fit-content(theme(spacing.32))]">
  <!-- even possible to use the `theme` function -->
</div>

<div class="bg-[--my-color]">
  <!-- not use var(...) to using a CSS variable as arbitrary value  -->
</div>
<div class="[mask-type:luminance]">
  <div>
    If you ever need to use a CSS property that Tailwind doesn’t include a
    utility for out of the box, you can also use square bracket notation to
    write completely arbitrary CSS
  </div>
</div>

<div class="[--zowie:red] dark:[--zowie:blue">
  <div class="text-[--zowie]">hello zowie</div>
</div>
<ul role="list">
  {#each items as item}
  <li class="lg:[&:nth-child(3)]:hover:underline">{item}</li>
  {/each}
</ul>

equal to

@media (min-width: 1024px) {
  .lg\:\[\&\:nth-child\(3\)\]\:hover\:underline:hover:nth-child(3) {
    text-decoration-line: underline;
  }
}
<div class="grid grid-cols-[1fr_500px_2fr]">
  <!-- When an arbitrary value needs to contain a space, use an underscore (_) instead and Tailwind will automatically convert it to a space at build-time -->
</div>

<div class="before:content-['hello\_world']">
  <!--  rare case that you actually need to use an underscore -->
</div>

<div className={String.raw`before:content-['hello\_world']`}>
  <!-- ... -->
</div>
<!-- Will generate a font-size utility -->
<div class="text-[22px]">...</div>

<!-- Will generate a color utility -->
<div class="text-[#bada55]">...</div>

<!-- But... -->
<div class="text-[var(--my-var)]">...</div>

<!-- In these situations, you can “hint” the underlying type to Tailwind by adding a CSS data type before the value: -->

<!-- Will generate a font-size utility -->
<div class="text-[length:var(--my-var)]">...</div>

<!-- Will generate a color utility -->
<div class="text-[color:var(--my-var)]">...</div>

Using CSS and @layer

When you need to add truly custom CSS rules to a Tailwind project, the easiest approach is to just add the custom CSS to your stylesheet, For more power, you can also use the @layer directive to add styles to Tailwind’s base, components, and utilities layers:

First of all, we need to know why does tailwind group styles into layers? In CSS, the order of the rules in your stylesheet decides which declaration wins when two selectors have the same specificity:

<style>
  .btn {
    background: blue;
    /* ... */
  }

  .bg-black {
    background: black;
  }
</style>

<!-- Here, both buttons will be black since .bg-black comes after .btn in the CSS: -->
<button class="btn bg-black">...</button>
<button class="bg-black btn">...</button>

To manage this, Tailwind organizes the styles it generates into three different “layers” — a concept popularized by ITCSS.

The @layer base directive is used to add styles to the base layer, which includes styles that are applied to the HTML elements by default. In the example, we’re setting the font family, font size, and line height for the body element.
The base layer is for things like reset rules or default styles applied to plain HTML elements.

@tailwind base;

/* Adding styles to the base layer */
@layer base {
  body {
    line-height: 1.5;
  }
}

The @layer components directive is used to add styles to the components layer, which includes reusable components such as buttons, cards, and forms. In the example, we’re adding styles to a button component with a class of .btn.
The components layer is for class-based styles that you want to be able to override with utilities.

@tailwind components;
/* Adding styles to the components layer */
@layer components {
  .my-custom-style {
    /* ... */
    @apply rounded-b-lg shadow-md;
  }
}

By using @apply, you can create new styles that are consistent with your existing utility classes, and easily modify those styles across your entire application.

The @layer utilities directive is used to add styles to the utilities layer, which includes utility classes that can be applied to any element to modify its appearance. In the example, we’re adding a utility class called .truncate that can be used to truncate text that overflows its container.
The utilities layer is for small, single-purpose classes that should always take precedence over any other styles.

@tailwind utilities;
/* Adding styles to the utilities layer */
@layer utilities {
  .truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .content-auto {
    content-visibility: auto;
  }
}

Being explicit about this makes it easier to understand how your styles will interact with each other, and using the @layer directive lets you control the final declaration order while still organizing your actual code in whatever way you like.

Add base styles

Adding component classes

Use the components layer for any more complicated classes you want to add to your project that you’d still like to be able to override with utility classes.

Adding custom utilities

Add any of your own custom utility classes to Tailwind’s utilities layer.

Using modifiers with custom CSS

Any custom styles you add to Tailwind with @layer will automatically support Tailwind’s modifier syntax for handling things like hover states, responsive breakpoints, dark mode, and more.

<style>
  @tailwind base;
  @tailwind components;
  @tailwind utilities;

  @layer utilities {
    .content-auto {
      content-visibility: auto;
    }
  }
</style>

<div class="lg:dark:content-auto">
  <!-- ... -->
</div>

Removing unused custom CSS

Any custom styles you add to the base, components, or utilities layers will only be included in your compiled CSS if those styles are actually used in your HTML.
If you want to add some custom CSS that should always be included, add it to your stylesheet without using the @layer directive.

Using multiple CSS files

If you are writing a lot of CSS and organizing it into multiple files, make sure those files are combined into a single stylesheet before processing them with Tailwind, or you’ll see errors about using @layer without the corresponding @tailwind directive.

The easiest way to do this is using the postcss-import plugin.

Writing plugins

You can also add custom styles to your project using Tailwind’s plugin system instead of using a CSS file(tailwind.config.cjs):

const plugin = require('tailwindcss/plugin');

module.exports = {
  // ...
  plugins: [
    plugin(function ({
      addBase,
      addComponents,
      addUtilities,
      theme,
      e,
      config,
    }) {
      addBase({
        h1: {
          fontSize: theme('fontSize.2xl'),
        },
        h2: {
          fontSize: theme('fontSize.xl'),
        },
      });
      addComponents({
        '.card': {
          backgroundColor: theme('colors.white'),
          borderRadius: theme('borderRadius.lg'),
          padding: theme('spacing.6'),
          boxShadow: theme('boxShadow.xl'),
        },
      });
      addUtilities({
        '.content-auto': {
          contentVisibility: 'auto',
        },
      });
    }),
  ],
};

Learn more about writing your own plugins in the Plugins documentation.

2023-06-23T14:40:36.742Z