sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.63k stars 4.13k forks source link

Add CSS @layer support for components, tailwind v4 #11345

Open techmunk opened 5 months ago

techmunk commented 5 months ago

Describe the problem

The tailwind v4 alpha uses native cascade layers @layer which can be read about on the MDN docs.

The component CSS that is rendered by sveltkit always puts CSS in the anonymous layer, which always takes precedence.

For example, a third party svelte component that looks like this:

<script>
  const { class: clazz = '' } = $props();
</script>

<div class="blue { clazz }">WORDS</div>

<style>
  :where(.blue) {
    color: white;
    background: blue;
  }
</style>

And used like this

<script>
  import Test from '$lib/test.svelte';

  import './styles.css';
</script>

<Test class="bg-red-500" />

With styles.css being

@import "tailwindcss";

Will result in the background always being blue due to how CSS layers work with all the tailwind utility classes being the in the @utilities, layer. I feel this is somewhat unexpected to a developer using a component with tailwind, especailly given the component author has tried to remove the problem by using the :where pseudo class.

Describe the proposed solution

I'm wondering if an option should be added to the sveltekit config to allow component CSS to be wrapped in a layer so it can be handled better with native CSS layers.

I've had success using this preprocess function (in Svelte 5)

function svelteCssLayer(layerName) {
    return {
        name: 'svelte-css-layer',
        style: ({ content }) => {
            return {
                code: `@layer ${layerName} { ${content} }`
            }
        },
    }
}

My svelte.config.js file then looks like this.

/** @type {import('@sveltejs/kit').Config} */
export default {
    preprocess: [svelteCssLayer('svelte'), vitePreprocess()],
    kit: {
        adapter: adapter()
    }
};

And my styles.css changes to:

@layer theme, base, svelte, components, utilities;

@import "tailwindcss";

I have a codesandbox here that puts it all together.

Alternatives considered

No response

Importance

nice to have

Additional Information

I think as tailwind v4 adoption grows, this is likely to become a larger issue. I'm not sure if this should target sveltekit, or svelte itself but I felt this interesting enough to raise an issue about.

iolyd commented 5 months ago

Attempting to approach this in svelte 5 with svelte-kit 2 and the proposed alternative preprocessing doesn't seem to work as expected. If the preprocessor only declares the css layer component styles should target, it appears to supersede the root css's declared layer order. In the case of tailwind v4, this means the base (preflight) layer ends up applying resets over component styles.

/**
 * @param {string} layerName
 * @see https://github.com/sveltejs/svelte/issues/11345
 */
function csslayer(layerName) {
  return {
    name: 'svelte-css-layer',
    style: ({ content }) => {
      return {
        code: `@layer ${layerName} { ${content} }`,
      };
    },
  };
}

/**
 * @type {import('@sveltejs/kit').Config}
 */
const config = {
  extensions: ['.svelte'],
  preprocess: [csslayer('components')],
  kit: {
    adapter: adapter(),
  },
};

export default config;
/* app.css */
@layer base, components, utilities;

@import 'tailwindcss/preflight' layer(base);
@import 'tailwindcss/utilities' layer(utilities);

@theme {...}
<!-- +layout.svelte -->
<script lang="ts">
  import '../app.css';
  let { children } = $props();
</script>

{@render children()}

results in this layer ordering when inspecting the DOM:

3: implicit outer layer
  0: components
  1: base
  2: utilities

but re-specifying the order in the preprocessor works

{ //...
  style: ({ content }) => {
    return {
      code: `@layer base; @layer ${layerName} { ${content} }`,
    };
  }
}
3: implicit outer layer
  0: base
  1: components
  2: utilities
techmunk commented 5 months ago

@iolyd it does appear that the component CSS is injected into the DOM prior to the app.css loaded by +layout.svelte, so the svelte layer ends up getting created first, and therefore has the lowest precedence.

Adding (from my example)

<style type="text/css">
  @layer base, theme, svelte, components, utilities;
</style>

in app.html above %sveltekit.head% seems to also resolve this.

I have not tried this in a preview build, but this is how the dev server seems to work.

techmunk commented 5 months ago

For my previous comment, this behaviour is described in the MDN docs in the last paragraph of this section https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Cascade_layers#the_layer_statement_at-rule_for_named_layers

Adding paragraph from MDN for context.

@layer theme, layout, utilities;

If the above statement is the first line of a site's CSS, the layer order will be theme, layout, and utilities. If some layers were created prior to the above statement, as long as layers with these names don't already exist, these three layers will be created and added to the end of the list of existing layers. However, if a layer with the same name already exists, then the above statement will create only two new layers. For example, if layout already existed, only theme and utilities will be created, but the order of layers, in this case, will be layout, theme, and utilities.

iolyd commented 5 months ago

@techmunk, yes I am aware of the CSS spec, though without any particular reason I expected global stylesheets imported in script tags to be processed and inserted to the DOM before component styles.