kit icon indicating copy to clipboard operation
kit copied to clipboard

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

Open tkhs0813 opened this issue 6 months ago • 10 comments

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.

tkhs0813 avatar Jun 11 '25 05:06 tkhs0813

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.

flayks avatar Jun 11 '25 10:06 flayks

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

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

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

teemingc avatar Jun 12 '25 02:06 teemingc

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.

teemingc avatar Jun 12 '25 08:06 teemingc

@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

flayks avatar Jun 12 '25 08:06 flayks

@eltigerchino Here is my component Button.svelte:

<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:

...

<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?

teemingc avatar Jun 12 '25 10:06 teemingc

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 🤔

flayks avatar Jun 12 '25 10:06 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 🤔

We only inline styles after build so that's why it always works during dev.

I wasn't able to reproduce the styles not applying as I can see there's inlined styles after I build the app. Is there any way you can strip down your current project to share a minimal reproduction?

teemingc avatar Jun 13 '25 01:06 teemingc

@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?

flayks avatar Jun 13 '25 14:06 flayks

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.

tkhs0813 avatar Jun 16 '25 01:06 tkhs0813

@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!

flayks avatar Jul 10 '25 18:07 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.

flayks avatar Jul 10 '25 19:07 flayks

@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. https://github.com/sveltejs/kit/pull/14007

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

tkhs0813 avatar Jul 17 '25 07:07 tkhs0813

Hey just checking in, is there a hope for this to get fixed? 🙌

flayks avatar Aug 31 '25 21:08 flayks

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).

teemingc avatar Sep 02 '25 01:09 teemingc

@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:

<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.

teemingc avatar Sep 11 '25 07:09 teemingc