Skip to content

Some CSS styles not applied with non-zero inlineStyleThreshold since v2.21.3 #13878

Closed

Description

@tkhs0813

Describe the bug

Since SvelteKit v2.21.3, when inlineStyleThreshold is set to a non-zero value, some CSS styles are not being applied correctly.

Reproduction

Logs

System Info

System:
    OS: macOS 15.5
    CPU: (12) arm64 Apple M3 Pro
    Memory: 708.78 MB / 36.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.16.0 - ~/.volta/tools/image/node/22.16.0/bin/node
    npm: 10.9.2 - ~/.volta/tools/image/node/22.16.0/bin/npm
  Browsers:
    Chrome: 137.0.7151.69
    Safari: 18.5
  npmPackages:
    @sveltejs/adapter-node: 5.2.12 => 5.2.12 
    @sveltejs/kit: 2.21.4 => 2.21.4 
    @sveltejs/vite-plugin-svelte: 5.1.0 => 5.1.0 
    svelte: 5.33.19 => 5.33.19 
    vite: 6.3.5 => 6.3.5

Severity

serious, but I can work around it

Additional Information

Setting inlineStyleThreshold: 0 resolves the issue.

Activity

  1. changed the title [-]CSS inlining regression after PR #13723: shared SCSS files not inlined when inlineStyleThreshold > 0[/-] [+]CSS inlining regression after PR #13723: SCSS files not inlined when inlineStyleThreshold > 0[/+] on Jun 11, 2025
  2. changed the title [-]CSS inlining regression after PR #13723: SCSS files not inlined when inlineStyleThreshold > 0[/-] [+]CSS inlining regression sveltekit@2.21.3: SCSS files not inlined when inlineStyleThreshold > 0[/+] on Jun 11, 2025
  3. changed the title [-]CSS inlining regression sveltekit@2.21.3: SCSS files not inlined when inlineStyleThreshold > 0[/-] [+]CSS inlining regression after sveltekit@2.21.3: SCSS files not inlined when inlineStyleThreshold > 0[/+] on Jun 11, 2025
  4. changed the title [-]CSS inlining regression after sveltekit@2.21.3: SCSS files not inlined when inlineStyleThreshold > 0[/-] [+]Some CSS styles not applied with non-zero inlineStyleThreshold since v2.21.3[/+] on Jun 11, 2025
  5. flayks commented on Jun 11, 2025

    @flayks

    I can confirm this bug as I thought I was hallucinating 😅
    It is not styling my Button component (which is normally imported, not dynamically) with the inlineStyleThreshold config option to 4096. It used to work a week ago and since this version, it doesn't build the style of my component when building.

    At the moment to keep this working I had to lower the threshold to 1024.

  6. elliott-with-the-longest-name-on-github commented on Jun 11, 2025

    @elliott-with-the-longest-name-on-github
    Contributor

    @eltigerchino is this related to that dynamic component styling PR?

  7. teemingc commented on Jun 12, 2025

    @teemingc
    Member

    @eltigerchino is this related to that dynamic component styling PR?

    Probably. I'll have a look and try to find a reproduction.

  8. teemingc commented on Jun 12, 2025

    @teemingc
    Member

    I can confirm this bug as I thought I was hallucinating 😅 It is not styling my Button component (which is normally imported, not dynamically) with the inlineStyleThreshold config option to 4096. It used to work a week ago and since this version, it doesn't build the style of my component when building.

    At the moment to keep this working I had to lower the threshold to 1024.

    Are you able to create a minimal reproduction? Or if you're unable to, could you share the styles? How were the styles added? In thee Svelte style block? Or imported as a CSS file in the Svelte script tag? Or how the component was imported into the page/layout?

    I'm finding this difficult to reproduce at the moment. A few things I've tried:

    1. Inlining CSS for the page component.
    2. Importing a component and inlining that CSS.
    3. Importing a component which imports another component and inlining that CSS.

    Let me know if there are other cases I should try. I wonder if CSS syntax plays a part here.

  9. flayks commented on Jun 12, 2025

    @flayks

    @eltigerchino Here is my component Button.svelte:

    <style lang="scss" src="./Button.scss"></style>
    
    <script lang="ts">
        import type { Snippet } from 'svelte'
    
        let {
            label,
            href,
            type,
            withIcon = false,
            icon = '+',
            disabled = false,
            isActive = false,
            isInactive = false,
            children,
            class: className,
            onclick,
        }: {
            label: string | null
            href?: string
            type?: 'submit' | 'reset' | 'button'
            withIcon?: boolean
            icon?: string
            disabled?: boolean
            isActive?: boolean
            isInactive?: boolean
            class?: string
            onclick?: any
            children?: Snippet
        } = $props()
    
        const tag = $derived(href ? 'a' : 'button')
        const isExternal = $derived(href && /^https?:\/\//i.test(href))
        const classes = $derived([
            'button',
            'text-caps',
            className,
            isActive && 'is-active',
            isInactive && 'is-inactive',
        ])
    </script>
    
    <svelte:element
        this={tag}
        {href}
        type={tag === 'button' ? type : undefined}
        class={classes}
        disabled={tag === 'button' ? disabled : null}
        tabindex={tag === 'button' ? 0 : null}
        target={isExternal ? '_blank' : undefined}
        rel={isExternal ? 'noreferrer noopener' : undefined}
        data-sveltekit-noscroll={!href ? undefined : href && !isExternal ? '' : undefined}
        role={tag === 'button' ? 'button' : undefined}
        {onclick}
    >
        {@render children?.()}
    
        {#if label}
            <span class="label" data-label={label}>
                <span>{label}</span>
            </span>
        {/if}
        {#if withIcon && icon}
            <span class="icon">{icon}</span>
        {/if}
    </svelte:element>

    And styles:

    .button {
        --sides: 12px;
        --duration: 0.35s;
        --color: var(--color-dark);
        position: relative;
        display: inline-flex;
        align-items: center;
        height: 36px;
        padding: 0 var(--sides);
        overflow: hidden;
        text-align: left;
        white-space: nowrap;
        color: var(--color);
        line-height: 1;
        text-decoration: none;
        background: none;
        border: none;
        box-shadow: inset 0 0 0 1px var(--color);
        appearance: none;
        cursor: pointer;
        transition-property: background-color, box-shadow, color, opacity;
        transition-duration: var(--duration);
        transition-timing-function: var(--ease-quart-inout);
    
        // State: Disabled
        &:disabled {
            opacity: 0.25;
            cursor: not-allowed;
        }
    
        // State: Hover
        &:not(:disabled):not(.is-active):hover {
            background-color: var(--color-dark);
            color: var(--color-white);
    
            .label {
                transform: translateY(2.5em);
            }
            &:after {
                transform: translateY(0);
            }
        }
    
        // State: Pressing
        &:active {
            .label {
                opacity: 0.65;
            }
        }
    
        // State: Active
        &.is-active {
            background-color: var(--color-dark);
            color: var(--color-white);
    
            .label {
                transform: translateY(2.5em);
            }
            &:after {
                transform: translateY(0);
            }
        }
    
        // State: Is inactive
        &.is-inactive {
            --color: var(--color-dark-20);
            color: var(--color-dark);
        }
    
        // Label
        .label {
            position: relative;
            transition-property: transform, opacity;
            transition-duration: var(--duration);
            transition-timing-function: var(--ease-quart-inout);
    
            span {
                white-space: nowrap;
            }
    
            &:after {
                content: attr(data-label);
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                transform: translateY(-2.5em);
            }
        }
    
        // Icon
        .icon {
            display: flex;
            align-items: center;
            height: 100%;
            margin-left: var(--sides);
            margin-right: calc(var(--sides) * -1 / 2);
            padding-left: 0.5em;
            border-left: 1px solid currentColor;
            font-size: 1.25em;
            font-weight: 500;
            transition: border-color var(--duration) var(--ease-quart-inout);
        }
    }

    Then imported that way, as simply as it can be:

    <script lang="ts">
        import Button from '$components/atoms/Button/Button.svelte'
    </script>
    
    ...
    
    <Button
        withIcon
        type="submit"
        label={isLoading ? 'Processing…' : 'Inquire'}
        disabled={isLoading}
    />

    Using this config:

    ...
    
    const config = {
        preprocess: sveltePreprocess({
            scss: {
                prependData: globalStyles,
            }
        }),
    
        kit: {
            adapter: adapter({
                routes: {
                    include: ['/*'],
                    exclude: ['<all>'],
                },
            }),
            alias: {
                $components: 'src/components',
                $layouts: 'src/layouts',
                $animations: 'src/animations',
                $utils: 'src/utils',
                $stores: 'src/stores',
            },
            prerender: {
                handleMissingId: 'ignore',
            },
            inlineStyleThreshold: 4096,
            // ...
    }

    What is weird is that this exact code that I haven't changed works and build styles correctly on 2.21.2 but not on 2.21.3 or 2.21.4

  10. teemingc commented on Jun 12, 2025

    @teemingc
    Member

    @eltigerchino Here is my component Button.svelte:

    <style lang="scss" src="./Button.scss"></style> <script lang="ts"> import type { Snippet } from 'svelte' let { label, href, type, withIcon = false, icon = '+', disabled = false, isActive = false, isInactive = false, children, class: className, onclick, }: { label: string | null href?: string type?: 'submit' | 'reset' | 'button' withIcon?: boolean icon?: string disabled?: boolean isActive?: boolean isInactive?: boolean class?: string onclick?: any children?: Snippet } = $props() const tag = $derived(href ? 'a' : 'button') const isExternal = $derived(href && /^https?:\/\//i.test(href)) const classes = $derived([ 'button', 'text-caps', className, isActive && 'is-active', isInactive && 'is-inactive', ]) </script>

    <svelte:element
    this={tag}
    {href}
    type={tag === 'button' ? type : undefined}
    class={classes}
    disabled={tag === 'button' ? disabled : null}
    tabindex={tag === 'button' ? 0 : null}
    target={isExternal ? '_blank' : undefined}
    rel={isExternal ? 'noreferrer noopener' : undefined}
    data-sveltekit-noscroll={!href ? undefined : href && !isExternal ? '' : undefined}
    role={tag === 'button' ? 'button' : undefined}
    {onclick}

    {@render children?.()}
    
    {#if label}
        <span class="label" data-label={label}>
            <span>{label}</span>
        </span>
    {/if}
    {#if withIcon && icon}
        <span class="icon">{icon}</span>
    {/if}
    

    </svelte:element>

    And styles:

    .button {
    --sides: 12px;
    --duration: 0.35s;
    --color: var(--color-dark);
    position: relative;
    display: inline-flex;
    align-items: center;
    height: 36px;
    padding: 0 var(--sides);
    overflow: hidden;
    text-align: left;
    white-space: nowrap;
    color: var(--color);
    line-height: 1;
    text-decoration: none;
    background: none;
    border: none;
    box-shadow: inset 0 0 0 1px var(--color);
    appearance: none;
    cursor: pointer;
    transition-property: background-color, box-shadow, color, opacity;
    transition-duration: var(--duration);
    transition-timing-function: var(--ease-quart-inout);

    // State: Disabled
    &:disabled {
        opacity: 0.25;
        cursor: not-allowed;
    }
    
    // State: Hover
    &:not(:disabled):not(.is-active):hover {
        background-color: var(--color-dark);
        color: var(--color-white);
    
        .label {
            transform: translateY(2.5em);
        }
        &:after {
            transform: translateY(0);
        }
    }
    
    // State: Pressing
    &:active {
        .label {
            opacity: 0.65;
        }
    }
    
    // State: Active
    &.is-active {
        background-color: var(--color-dark);
        color: var(--color-white);
    
        .label {
            transform: translateY(2.5em);
        }
        &:after {
            transform: translateY(0);
        }
    }
    
    // State: Is inactive
    &.is-inactive {
        --color: var(--color-dark-20);
        color: var(--color-dark);
    }
    
    // Label
    .label {
        position: relative;
        transition-property: transform, opacity;
        transition-duration: var(--duration);
        transition-timing-function: var(--ease-quart-inout);
    
        span {
            white-space: nowrap;
        }
    
        &:after {
            content: attr(data-label);
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            transform: translateY(-2.5em);
        }
    }
    
    // Icon
    .icon {
        display: flex;
        align-items: center;
        height: 100%;
        margin-left: var(--sides);
        margin-right: calc(var(--sides) * -1 / 2);
        padding-left: 0.5em;
        border-left: 1px solid currentColor;
        font-size: 1.25em;
        font-weight: 500;
        transition: border-color var(--duration) var(--ease-quart-inout);
    }
    

    }

    Then imported that way, as simply as it can be:

    <script lang="ts"> import Button from '$components/atoms/Button/Button.svelte' </script>

    ...

    <Button
    withIcon
    type="submit"
    label={isLoading ? 'Processing…' : 'Inquire'}
    disabled={isLoading}
    />

    Using this config:

    ...

    const config = {
    preprocess: sveltePreprocess({
    scss: {
    prependData: globalStyles,
    }
    }),

    kit: {
        adapter: adapter({
            routes: {
                include: ['/*'],
                exclude: ['<all>'],
            },
        }),
        alias: {
            $components: 'src/components',
            $layouts: 'src/layouts',
            $animations: 'src/animations',
            $utils: 'src/utils',
            $stores: 'src/stores',
        },
        prerender: {
            handleMissingId: 'ignore',
        },
        inlineStyleThreshold: 4096,
        // ...
    

    }

    What is weird is that this exact code that I haven't changed works and build styles correctly on 2.21.2 but not on 2.21.3 or 2.21.4

    Thank you! I've tried to reproduce this by adding sveltePreprocss, sass, and the scss + svelte code but I'm not sure what I'm suppose to expect. Can you share a screenshot of what the button looks like working/broken? Also, I'm missing some of the global styles the scss references like --color-dark, so I don't know if my comparison will be accurate. However, I can see the styles inlined in the <head>. I just don't know if the issue is that the styles are not inlined or if the styles are inlined but not applying correctly?

  11. flayks commented on Jun 12, 2025

    @flayks

    It simply doesn't compile or applies the styles for this component for some reason. While running dev, it's all good but when building, it just renders the button as the default style 🤔

  12. 1 remaining item

  13. flayks commented on Jun 13, 2025

    @flayks

    @eltigerchino I tried to replicate it on a smaller setup by just keeping a <Button /> in a <main> within a +page.svelte and it builds fine for some reason, with the same Svelte config. This is quite weird! I'll keep investigating but I suspect that a bigger site with many more components can cause some sort of a threshold with the inlineStyle setting?

  14. tkhs0813 commented on Jun 16, 2025

    @tkhs0813
    ContributorAuthor

    I’m trying to reproduce the issue with the minimal setup, but I haven’t been able to reproduce it yet.
    I’m using SCSS for styling and not using dynamic imports.

  15. flayks commented on Jul 10, 2025

    @flayks

    @tkhs0813 @eltigerchino I gave another go and it seems that only one component is affected in my case, which is weird.

    I think I found the issue, or at least a case where it happens: using this component in that route routes/product/[product]/+page.svelte without having a root page for /product seems to be the culprit. I literally have nothing else than <Button href="/page" label="Test" /> in the page and the styles are not being built.

    Is this of any help? Or does that make any sense? 😅

    The only way to make this work with inlineStyleThreshold is to lower the value to 2048.

    The component in question:

    <style lang="scss" src="./Button.scss"></style>
    
    <script lang="ts">
        import type { Snippet } from 'svelte'
    
        let {
            label,
            href,
            type,
            withIcon = false,
            icon = '+',
            disabled = false,
            isActive = false,
            isInactive = false,
            children,
            class: className,
            onclick,
        }: {
            label: string | null
            href?: string
            type?: 'submit' | 'reset' | 'button'
            withIcon?: boolean
            icon?: string
            disabled?: boolean
            isActive?: boolean
            isInactive?: boolean
            class?: string
            onclick?: any
            children?: Snippet
        } = $props()
    
        const tag = $derived(href ? 'a' : 'button')
        const isExternal = $derived(href && /^https?:\/\//i.test(href))
    </script>
    
    <svelte:element
        this={tag}
        {href}
        type={tag === 'button' ? type : undefined}
        class={['button text-caps', className]}
        class:is-active={isActive}
        class:is-inactive={isInactive}
        disabled={tag === 'button' ? disabled : null}
        tabindex={tag === 'button' ? 0 : null}
        target={isExternal ? '_blank' : undefined}
        rel={isExternal ? 'noreferrer noopener' : undefined}
        data-sveltekit-noscroll={!href ? undefined : href && !isExternal ? '' : undefined}
        role={tag === 'button' ? 'button' : undefined}
        {onclick}
    >
        {@render children?.()}
    
        {#if label}
            <span class="label" data-label={label}>
                <span>{label}</span>
            </span>
        {/if}
        {#if withIcon && icon}
            <span class="icon">{icon}</span>
        {/if}
    </svelte:element>

    Could it be because of the <svelte:element>?

    EDIT: having refactored the component to use a content() snippet and conditions based on the tag, it's not that!

  16. flayks commented on Jul 10, 2025

    @flayks

    Another case where all the styles would build including my nested <Button /> component: when I do NOT add the stylesheet for my routes/product/[product]/+page.svelte, or have an empty stylesheet:

    <style lang="scss" src="../../../styles/pages/product.scss"></style>

    I feel there is a bug somewhere related to stylesheets with dynamic routes.

  17. tkhs0813 commented on Jul 17, 2025

    @tkhs0813
    ContributorAuthor

    @eltigerchino
    For your reference:
    We still haven’t been able to reproduce the issue with a minimal setup, but we were able to resolve the missing styles problem in our application by using Cursor.
    #14007

    However, we're not sure if this fix is actually appropriate.

  18. teemingc commented on Sep 2, 2025

    @teemingc
    Member

    Sorry, this kinda got lost in the backlog for me personally as I've had to prioritise some non-SvelteKit work last month. I'll try to see if I can squeeze some time in for this this week or next (most likely).

  19. teemingc commented on Sep 11, 2025

    @teemingc
    Member

    @tkhs0813 @eltigerchino I gave another go and it seems that only one component is affected in my case, which is weird.

    I think I found the issue, or at least a case where it happens: using this component in that route routes/product/[product]/+page.svelte without having a root page for /product seems to be the culprit. I literally have nothing else than <Button href="/page" label="Test" /> in the page and the styles are not being built.

    Is this of any help? Or does that make any sense? 😅

    The only way to make this work with inlineStyleThreshold is to lower the value to 2048.

    The component in question:

    <style lang="scss" src="./Button.scss"></style> <script lang="ts"> import type { Snippet } from 'svelte' let { label, href, type, withIcon = false, icon = '+', disabled = false, isActive = false, isInactive = false, children, class: className, onclick, }: { label: string | null href?: string type?: 'submit' | 'reset' | 'button' withIcon?: boolean icon?: string disabled?: boolean isActive?: boolean isInactive?: boolean class?: string onclick?: any children?: Snippet } = $props() const tag = $derived(href ? 'a' : 'button') const isExternal = $derived(href && /^https?:\/\//i.test(href)) </script>

    <svelte:element
    this={tag}
    {href}
    type={tag === 'button' ? type : undefined}
    class={['button text-caps', className]}
    class:is-active={isActive}
    class:is-inactive={isInactive}
    disabled={tag === 'button' ? disabled : null}
    tabindex={tag === 'button' ? 0 : null}
    target={isExternal ? '_blank' : undefined}
    rel={isExternal ? 'noreferrer noopener' : undefined}
    data-sveltekit-noscroll={!href ? undefined : href && !isExternal ? '' : undefined}
    role={tag === 'button' ? 'button' : undefined}
    {onclick}

    {@render children?.()}
    
    {#if label}
        <span class="label" data-label={label}>
            <span>{label}</span>
        </span>
    {/if}
    {#if withIcon && icon}
        <span class="icon">{icon}</span>
    {/if}
    

    </svelte:element>

    Could it be because of the <svelte:element>?

    EDIT: having refactored the component to use a content() snippet and conditions based on the tag, it's not that!

    I can't reproduce this in https://stackblitz.com/github/eltigerchino/13878

    If both of you really can't create a minimal reproduction, if you're open to it, you could temporarily share the private repository with me and I can give it a try too.

  20. timephy commented on Dec 12, 2025

    @timephy

    I have created a minimal reproduction for my experience of this issue:

    ⚠️ The bug only happens when running pnpm run build && pnpm run preview (with inlineStyleThreshold > 0) and NOT during pnpm run dev!

    <script lang="ts">
        import Nugget from "./Nugget.svelte"
    
        let show = $state(false)
    </script>
    
    <section>
        <button onclick={() => (show = !show)}>Toggle Show</button>
    
        {#if show}
            <Nugget text="abc" id={null} />
        {/if}
    </section>
    
    <!-- 1. With this `<style>` "active", then Nugget's `<style>` IS NOT applied ❌ -->
    <style>
        section {
            display: flex;
            flex-direction: column;
            align-items: center;
        }
    </style>
    
    <!-- 2. With an empty `<style>` or no `<style>` at all, then Nugget's `<style>` IS applied ✅ -->
    <!-- <style>
    </style> -->
    
    <!-- 3. This causes a weird "Server stylesheet source not found for client stylesheet" error (maybe unrelated) -->
    <!-- <style>
        /*  */
    </style> -->
    

    The entire repo is available here: https://github.com/timephy/sveltekit-inlineStyleThreshold-bug


    I have been struggling with this for the duration of this open issue. I have set inlineStyleThreshold: 0 to fix this in the mean time.

    I hope this helps you @teemingc to debug this problem, I would appreciate a fix a lot!

    I'll leave some things I observed here:

    • The {#if} block is mandatory for this bug to occur
    • An existing and non-empty <style> block is required in the component that has the {#if} block
    • This has nothing to do with the postcss-tailwind-@apply, it also happens with "native css"

    Also:

    • This might not only happen with {#if} blocks, but maybe also with {#each ...} block, etc...

    I have a large code base where this issue appears in multiple places... I'd be happy to test a fix against it! 👍🏽

  21. timephy commented on Jan 7, 2026

    @timephy
  22. teemingc commented on Jan 7, 2026

    @teemingc
    Member

    Sorry this was delayed for so long. The recent reproduction is incredibly helpful. I've started investigating and should be able to come up with something by next week as I will be maintaining SvelteKit full time thanks to the Svelte Open Collective (however, I'm currently on leave this week).

    Some notes so far:

    1. An SSR-ed page with no styles fetches the css of the conditionally rendered components only when it's first rendered
    2. If the SSR-ed page has styles, it is inlined. However, those styles exclude the conditionally rendered component styles because the component is not rendered during SSR. It is also not fetched later when it's first rendered because the styles for every component in the page is being grouped together on the client-side. This causes client to think that the styles have already been loaded when we inline the server styles.
  23. added a commit that references this issue on Jan 10, 2026
    0cdd9e4
  24. timephy commented on Jan 28, 2026

    @timephy

    Thank you guys!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions