Color Mode

@import and @layer: Managing CSS at Scale

As CSS projects grow, organizing code across multiple files and managing specificity conflicts becomes critical. You have learned modern selectors like :where() that reduce specificity, and you have learned architecture patterns like ITCSS that organize CSS by specificity levels. However, these approaches still require discipline and careful planning. What if CSS provided built-in tools to explicitly control file loading and cascade layers?

The @import rule allows you to split CSS across multiple files and load them in order. The @layer rule creates explicit cascade layers that control specificity independent of selector complexity. Together, these features give you direct control over how CSS is organized and applied. This lesson explains what problems they solve, when to use them, and how they integrate with modern CSS workflows.

Reference Material

This lesson serves as a reference you can return to when organizing CSS projects. You do not need to memorize every detail now. Focus on understanding what problems @import and @layer solve and when you might use them. You will apply these concepts in your team project.

The Problem: CSS Organization at Scale

Consider a large project with separate files for base styles, layout patterns, components, and utilities. Without explicit organization tools, you face several challenges:

These problems multiply as teams grow and projects age. @import and @layer provide explicit solutions rather than relying on conventions and discipline alone.

@import: Loading CSS Files

The @import rule loads CSS from another file into your current stylesheet. This allows you to split CSS across multiple files while maintaining control over loading order from a single entry point.

Basic Syntax

Place @import rules at the top of your CSS file, before any other rules. The browser loads imported files in the order they appear.


        /* main.css - entry point */
        @import url('reset.css');
        @import url('variables.css');
        @import url('typography.css');
        @import url('layout.css');
        @import url('components.css');
        @import url('utilities.css');

        /* Additional styles can follow */
        body {
            font-family: var(--font-base);
        }
    

Why Use @import?

Using @import centralizes CSS loading in your stylesheet rather than your HTML. Your HTML needs only one <link> tag pointing to main.css, and that file controls loading order for all other stylesheets. This separation of concerns keeps HTML clean and makes reorganizing CSS file structure easier.

However, @import has an important limitation: it blocks rendering. The browser must download and process each imported file before it can continue, which can slow initial page load. For production sites, build tools typically bundle imported files into a single CSS file to avoid this performance penalty. During development, @import provides convenient organization.

Import with Media Queries

You can conditionally load CSS files based on media queries. This allows you to separate print styles, high-contrast styles, or responsive styles into dedicated files.


        @import url('screen.css') screen;
        @import url('print.css') print;
        @import url('high-contrast.css') (prefers-contrast: high);
    

When to Use @import

Use @import when you want to organize CSS across multiple files during development but plan to bundle them for production. It works well for:

Performance Consideration

Each @import creates a separate network request during development. While browser caching and modern CDN networks can help improve the experience of unbundled CSS, it is still good practice not to leave unbundled imports in production. For production, use build tools to bundle imported files into a single CSS file to minimize requests and ensure optimal performance.

@layer: Explicit Cascade Control

Cascade layers provide explicit control over specificity and source order. Instead of relying on careful selector writing and file organization, you declare layers that define precedence rules. Layers solve the core problem of CSS architecture: ensuring that the right styles win regardless of specificity or source order.

Understanding the Problem

Recall from modern selectors that :where() has zero specificity, allowing easy overrides. However, :where() only helps within individual selectors. What if you want entire files or sections of CSS to have lower priority than others? Traditional CSS relies on source order and specificity, making it fragile when adding new styles.

Consider this scenario: your base styles use :where() for low specificity, but a third-party component library has high-specificity selectors. Your utility classes should override everything, but the component library's specificity might be higher. With layers, you explicitly declare that utilities always win, regardless of specificity.

Defining Layers

Define layer order once at the top of your stylesheet. Layers listed first have lower priority; layers listed last have higher priority.


    /* Define layer order - first = lowest priority */
    @layer reset, base, layout, components, utilities;

    @layer reset {
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
    }

    @layer base {
        body {
            font-family: system-ui;
            line-height: 1.6;
            color: oklch(0.2 0 0);
        }
    }

    @layer components {
        .button {
            padding: 0.5rem 1rem;
            background: oklch(0.5 0.15 250);
            color: white;
        }
    }

    @layer utilities {
        .text-center {
            text-align: center !important;
        }
    }

In this example, utilities always override components, components always override base styles, and base styles always override reset styles, regardless of selector specificity within each layer. A simple class in the utilities layer beats a highly specific ID selector in the components layer.

How Layers Change Specificity

Within a layer, normal specificity rules apply. A class beats an element selector, an ID beats a class, and so on. However, between layers, layer order determines precedence. Even a low-specificity selector in a higher layer beats a high-specificity selector in a lower layer.


    @layer base, components;

    @layer base {
        #navigation ul li a {
            color: blue;
        }
        /* Specificity: 1,0,3 - but in base layer */
    }

    @layer components {
        a {
            color: red;
        }
        /* Specificity: 0,0,1 - but in components layer */
    }

    /* Result: Links are red because components layer wins */

This behavior solves the fundamental problem of CSS architecture. You no longer need to carefully manage specificity across your entire codebase. Instead, you organize styles into layers that reflect their purpose, and the layer order handles precedence automatically.

Combining @import with @layer

You can import entire files into specific layers, combining file organization with cascade control. This is where @import and @layer become powerful together.


    /* main.css */
    @layer reset, base, layout, components, utilities;

    @import url('reset.css') layer(reset);
    @import url('variables.css') layer(base);
    @import url('typography.css') layer(base);
    @import url('grid.css') layer(layout);
    @import url('buttons.css') layer(components);
    @import url('cards.css') layer(components);
    @import url('spacing.css') layer(utilities);
    @import url('text.css') layer(utilities);

This pattern gives you both file organization and cascade control. Each imported file automatically belongs to its specified layer, and you control layer precedence from a single location.

Unlayered Styles

Any styles not placed in a layer belong to the unlayered cascade, which has the highest priority. This means unlayered styles always win over layered styles. Use this for critical overrides or one-off fixes that need to supersede everything.


    @layer base, components;

    @layer components {
        .button {
            background: blue;
        }
    }

    /* Unlayered - always wins */
    .button.critical {
        background: red;
    }

Practical Organization Patterns

Now that you understand what @import and @layer do, here are practical patterns for organizing CSS in real projects.

ITCSS with Layers

The ITCSS (Inverted Triangle CSS) architecture you learned maps naturally to cascade layers. Each ITCSS level becomes a layer, with explicit precedence.


    /* Explicit ITCSS structure with layers */
    @layer settings, tools, generic, elements, objects, components, utilities;

    @import url('settings/variables.css') layer(settings);
    @import url('settings/colors.css') layer(settings);

    @import url('generic/reset.css') layer(generic);
    @import url('generic/box-sizing.css') layer(generic);

    @import url('elements/typography.css') layer(elements);
    @import url('elements/links.css') layer(elements);

    @import url('objects/container.css') layer(objects);
    @import url('objects/grid.css') layer(objects);

    @import url('components/button.css') layer(components);
    @import url('components/card.css') layer(components);
    @import url('components/navigation.css') layer(components);

    @import url('utilities/spacing.css') layer(utilities);
    @import url('utilities/text.css') layer(utilities);

CUBE CSS with Layers

CUBE CSS separates composition, utilities, blocks, and exceptions. Layers enforce that utilities override blocks, blocks override composition, and exceptions override utilities.


    @layer composition, blocks, utilities, exceptions;

    @import url('composition/flow.css') layer(composition);
    @import url('composition/grid.css') layer(composition);
    @import url('composition/cluster.css') layer(composition);

    @import url('blocks/card.css') layer(blocks);
    @import url('blocks/button.css') layer(blocks);

    @import url('utilities/colors.css') layer(utilities);
    @import url('utilities/spacing.css') layer(utilities);

    @import url('exceptions/states.css') layer(exceptions);

Third-Party CSS Integration

When using third-party CSS libraries, layers let you control where they sit in your cascade. You can ensure your utilities always override third-party components.


    @layer reset, vendor, base, components, utilities;

    @import url('normalize.css') layer(reset);
    @import url('third-party-lib.css') layer(vendor);
    @import url('my-base.css') layer(base);
    @import url('my-components.css') layer(components);
    @import url('my-utilities.css') layer(utilities);

Best Practices and Guidelines

Layer Naming Conventions

Choose layer names that reflect purpose rather than specificity levels. Names like components and utilities communicate intent. Avoid names like low-priority or high-priority that only describe cascade behavior.

When to Use Layers

Use cascade layers when:

When Not to Use Layers

Layers add complexity. Avoid them when:

Debugging Layer Issues

Browser DevTools show which layer a style belongs to. When debugging unexpected cascade behavior, check the Styles panel to see layer information. This helps you understand why certain styles apply or do not apply.

Connection to Modern Selectors

Earlier you learned that :where() has zero specificity, making it easy to override. You learned that :is() takes the highest specificity of its arguments. These selectors give you fine-grained specificity control within individual rules.

Cascade layers provide coarse-grained specificity control across entire sections of CSS. Use :where() within base layers to ensure easy overrides within that layer. Use :is() in component layers where you need normal specificity. Use layers to control precedence between these sections.

Together, modern selectors and cascade layers give you complete control over specificity:

This combination of tools makes CSS architecture more explicit and maintainable than ever before. You no longer need to rely solely on naming conventions and developer discipline to manage cascade behavior.

Check Your Understanding

Test your understanding of how @import and @layer work together to organize CSS. Analyze the code example and answer questions about cascade behavior and layer precedence.


    /**
     * Analyze this CSS organization:
     * 1. What is the layer precedence order (lowest to highest)?
     * 2. If both rules target the same button, which color wins?
     * 3. Why does layer order matter more than specificity here?
     * 4. When would you NOT use layers for a project?
     */

    @layer base, components, utilities;

    @layer base {
        button {
            color: blue;
        }
    }

    @layer utilities {
        .btn {
            color: red;
        }
    }

    /* Which color applies to:  */

Key Concepts Summary

The @import rule loads CSS files in sequence, allowing you to organize styles across multiple files while controlling load order from a single entry point. Use it during development for organization, but bundle imports for production to avoid performance penalties.

The @layer rule creates explicit cascade layers that control precedence independent of specificity. Layer order determines which styles win when conflicts occur, solving the core problem of CSS architecture by making precedence explicit rather than relying on careful selector writing.

Together, these features let you organize CSS files by purpose and enforce architecture patterns like ITCSS or CUBE CSS. When combined with modern selectors like :where() and :is(), you have complete control over how CSS is organized and applied at scale.

Further Resources

For detailed technical documentation and browser compatibility information:

Bookmark this page as a reference when you organize CSS in your team project. You do not need to memorize syntax details. Focus on understanding when these tools solve real problems in your workflow.