DEV Community

Cover image for Styling in Svelte (Scoped CSS, :global, and Class Directives)
Ali Aslam
Ali Aslam

Posted on

Styling in Svelte (Scoped CSS, :global, and Class Directives)

Why Styling Matters 🎨

Code makes apps work, but styling makes them delightful.
Imagine a to-do app that functions perfectly — but everything is plain Times New Roman, unstyled checkboxes, and buttons straight out of 1995. It works… but no one wants to use it.

That’s where styling comes in. A clean, well-designed UI makes your app feel modern, usable, and trustworthy.

Svelte doesn’t just give you reactive components — it also gives you a built-in styling model. Unlike React (where you often reach for extras like CSS Modules or styled-components), Svelte treats styles as a first-class citizen, right inside your .svelte files.

👉 By the end of this guide, you’ll be able to:

  • Write scoped CSS that applies only to one component.
  • Escape the scope with :global when needed.
  • Use class directives for dynamic, reactive styling.
  • Supercharge your workflow with Tailwind CSS utilities.

Let’s begin with the foundation: scoped styles — Svelte’s secret to avoiding CSS spaghetti.


Step 1: Scoped CSS — The Svelte Way

Every Svelte component (.svelte file) can contain a <style> block.
The key difference from plain HTML/CSS:

  • In regular HTML, a <style> tag applies globally across the whole page.
  • In Svelte, a <style> tag inside a component is scoped: it only affects the markup in that same file.

This is what makes component-based styling safe and predictable.


Example: A Styled Button

src/lib/Button.svelte

<script>
  // This is a reusable button component.
  // "label" is a prop passed in by the parent.
  export let label = "Click me";
</script>

<!-- The markup that will be styled -->
<button>{label}</button>

<!-- 
  đź”˝ This style tag belongs to *this component only*. 
  It won’t leak to other <button> elements elsewhere in your app.
-->
<style>
  button {
    background: royalblue;
    color: white;
    border: none;
    padding: 0.5rem 1rem;
    border-radius: 6px;
    cursor: pointer;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Using the Component

src/routes/+page.svelte

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

<h1>Scoped CSS Demo</h1>

<!-- Two buttons, styled identically -->
<Button label="Save" />
<Button label="Cancel" />

<!-- A plain HTML button (not styled by Button.svelte) -->
<button>Plain Button</button>
Enter fullscreen mode Exit fullscreen mode

âś… The <Button /> components have the royal blue styling.
✅ The plain <button> at the bottom looks unstyled — proving styles don’t leak out.
✅ You don’t need unique class names like .button-primary or .button-danger to avoid conflicts — scoping is automatic.


Step 2: How Scope Actually Works

So how does Svelte keep your styles from leaking out?

Behind the scenes, the compiler rewrites your CSS selectors to be unique per component.


Example: Your Code

If you write this in Button.svelte:

button {
  background: royalblue;
}
Enter fullscreen mode Exit fullscreen mode

What Svelte Compiles To

Svelte generates a random hash (like svelte-xyz123) and rewrites your code:

button.svelte-xyz123 {
  background: royalblue;
}
Enter fullscreen mode Exit fullscreen mode

And the <button> element in the DOM gets:

<button class="svelte-xyz123">Save</button>
Enter fullscreen mode Exit fullscreen mode

âś… Result: the style applies only to buttons in this component.
❌ Other <button>s elsewhere in your app aren’t affected.

You don’t need to worry about those svelte-xyz123 classes — Svelte manages them automatically. But knowing this explains why scoped CSS “just works.”


👉 This is why Svelte projects don’t descend into CSS spaghetti, where one stylesheet accidentally overrides another.


Step 3: Escaping Scope with :global

By default, styles inside a component are scoped — they only apply locally.
But sometimes, you really do need styles that affect the entire app:

  • A CSS reset (removing browser defaults).
  • Global typography (setting fonts and line-heights).
  • Theming (e.g. dark mode background).

That’s where :global comes in.


Example: A Global Reset

src/routes/+layout.svelte

<style>
  /* This targets the <body> across the whole app */
  :global(body) {
    margin: 0;
    font-family: system-ui, sans-serif;
    background: #fafafa;
  }
</style>

<slot />
Enter fullscreen mode Exit fullscreen mode

Now every page in your app inherits these global styles.


Example: Mixing Scoped and Global

You can also combine scoped selectors with global ones:

<style>
  /* Only <span> inside this component’s <button> are styled */
  button :global(span) {
    color: yellow;
  }
</style>

<button>
  Normal <span>highlighted</span> text
</button>
Enter fullscreen mode Exit fullscreen mode

Here, the <button> remains scoped, but the span inside it is styled using a global selector.


⚠️ Rule of thumb:
Stay scoped by default (safer, predictable).
Use :global only for resets, shared typography, or app-wide theming.


Step 4: Dynamic Classes with class: Directive

Static styles are nice, but real UIs are dynamic.
Think about:

  • A “selected” button in a toolbar.
  • An “active” tab in navigation.
  • A form field that turns red when invalid.

You don’t want to hard-code these classes — you want them to respond to state.


Example: Toggling an Active Class

src/lib/ToggleButton.svelte

<script>
  let active = false; // local state
</script>

<!-- 
  class:active={active} means:
  → add the class "active" when active === true
  → remove it when active === false
-->
<button
  class:active={active}
  on:click={() => active = !active}
>
  {active ? "Active" : "Inactive"}
</button>

<style>
  /* Normal style */
  button {
    padding: 0.5rem 1rem;
    border: none;
    cursor: pointer;
  }

  /* Extra style when active */
  .active {
    background: green;
    color: white;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

âś… Now when you click the button, the active class toggles on and off automatically.
❌ Without class:, you’d have to write something messy like:

<button class={active ? "active" : ""}>...</button>
Enter fullscreen mode Exit fullscreen mode

Svelte’s class: directive keeps it clean, reactive, and declarative.


Step 5: Multiple Class Directives

Sometimes a component can have different visual variants.
Think of buttons: primary, danger, success, outline… you don’t want to make a new component for each.

With Svelte, you can stack multiple class: directives to toggle different classes based on state or props.


Example: Button Variants

src/lib/VariantButton.svelte

<script>
  // Svelte 5 runes props
  let { type = "primary" } = $props(); // "primary" | "danger" | "success"
</script>

<button
  type="button"
  class:primary={type === "primary"}
  class:danger={type === "danger"}
  class:success={type === "success"}
>
  {type === "primary" ? "Save" : type === "danger" ? "Delete" : "OK"}
</button>

<style>
  button {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    color: white;
  }

  .primary {
    background: royalblue;
  }

  .danger {
    background: crimson;
  }

  .success {
    background: seagreen;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Usage

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

<VariantButton type="primary" />
<VariantButton type="danger" />
<VariantButton type="success" />
Enter fullscreen mode Exit fullscreen mode

âś… The button automatically switches between green blue and red depending on the type.
âś… You can add as many variants as you want (success, warning, etc).
❌ Without class directives, you’d need to manually concatenate classes, which quickly gets messy.


Step 6: Inline Styles

Most of the time you’ll use CSS classes for styling, but sometimes you need a quick dynamic style — like controlling the exact width, height, or color of an element from a variable.

Svelte lets you bind styles directly inside the style attribute.


Example: Dynamic Box

src/lib/DynamicBox.svelte

<script>
<script>
  // Reactive variable that controls size
  let size = $state(50);
</script>

<div
  style="width: {size}px; height: {size}px; background: coral;"
></div>

<button type="button" onclick={() => size += 20}>
  Grow Box
</button>
Enter fullscreen mode Exit fullscreen mode

âś… The box grows every time you click the button.
✅ Inline styles are fully reactive — any change to size updates the DOM instantly.

⚠️ Best practice:
Use inline styles only for one-off, highly dynamic values (like size, position, or transform).
For reusable styles (colors, padding, hover states), stick to CSS classes, which are easier to maintain and theme.


Quick Recap

So far, you’ve learned:

  • Svelte styles are scoped by default.
  • :global lets you escape scope for resets or shared rules.
  • class: directives toggle classes reactively.
  • Inline styles work for dynamic values.

You’ve now mastered the foundation of Svelte styling: scoped CSS, global overrides, class toggling, and inline styles. With these tools, you can already build clean, reusable components that look and behave consistently.

👉 In the next article, we’ll take things further with motion and theming - transitions, animations, and strategies for handling light/dark modes.

Top comments (0)