2.5 — Estilização: CSS com Escopo vs CSS-in-JS

CSS sem conflitos, sem bibliotecas, sem runtime.

Objetivos da Aula

  • Entender o escopo automático de CSS no Svelte
  • Comparar com soluções CSS-in-JS do React
  • Ver estilos dinâmicos, globais e variáveis CSS

O Problema: CSS Global

CSS tradicional é global por padrão:

/* styles.css */
.button {
  background: blue;
}

/* Outro arquivo qualquer */
.button {
  background: red;  /* CONFLITO! Quem ganha? */
}

Isso causa:

  • Conflitos de nomes de classes
  • Efeitos colaterais inesperados
  • CSS que não pode ser removido com segurança
  • Especificidade wars (!important everywhere)

Soluções no React

CSS Modules

// Button.module.css
.button {
  background: blue;
  padding: 10px;
}

.primary {
  background: purple;
}

// Button.jsx
import styles from './Button.module.css'

function Button({ primary, children }) {
  return (
    <button className={`${styles.button} ${primary ? styles.primary : ''}`}>
      {children}
    </button>
  )
}

// Gera: <button class="Button_button_x7y2z Button_primary_a1b2c">

styled-components

import styled from 'styled-components'

const StyledButton = styled.button`
  background: ${props => props.primary ? 'purple' : 'blue'};
  padding: 10px;
  color: white;
  border: none;

  &:hover {
    opacity: 0.8;
  }

  ${props => props.large && `
    padding: 20px;
    font-size: 1.2rem;
  `}
`

function App() {
  return <StyledButton primary large>Clique</StyledButton>
}

// Gera: <button class="sc-aBcDe xyz123">

Emotion

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'

const buttonStyle = css`
  background: blue;
  padding: 10px;
`

function Button() {
  return <button css={buttonStyle}>Clique</button>
}

// Ou inline
function Button() {
  return (
    <button
      css={css`
        background: blue;
        padding: 10px;
      `}
    >
      Clique
    </button>
  )
}

Tailwind CSS

function Button({ primary }) {
  return (
    <button
      className={`px-4 py-2 rounded ${
        primary
          ? 'bg-purple-600 text-white'
          : 'bg-blue-600 text-white'
      }`}
    >
      Clique
    </button>
  )
}

Svelte: Escopo Automático

No Svelte, CSS é automaticamente escopado ao componente:

<!-- Button.svelte -->
<button class="button primary">
  Clique
</button>

<style>
  /* Este CSS só afeta ESTE componente! */
  .button {
    background: blue;
    padding: 10px;
  }

  .primary {
    background: purple;
  }

  /* Seletores de elemento também são escopados */
  button {
    border: none;
    cursor: pointer;
  }
</style>

O que o compilador gera:

<button class="button primary svelte-xyz123">Clique</button>

<style>
  .button.svelte-xyz123 { background: blue; padding: 10px; }
  .primary.svelte-xyz123 { background: purple; }
  button.svelte-xyz123 { border: none; cursor: pointer; }
</style>

Vantagens:

  • ✅ Zero configuração
  • ✅ Zero runtime (CSS puro no output)
  • ✅ Sem conflitos de nomes
  • ✅ Código morto é removível
  • ✅ CSS fica junto do componente
  • ✅ Warnings se classes não forem usadas

Estilos Dinâmicos

React (styled-components)

const Box = styled.div`
  width: ${props => props.size}px;
  background: ${props => props.color};
  transform: rotate(${props => props.rotation}deg);
`

<Box size={100} color="red" rotation={45} />

Svelte

<script>
  export let size = 100
  export let color = 'red'
  export let rotation = 0
</script>

<div
  class="box"
  style:width="{size}px"
  style:background={color}
  style:transform="rotate({rotation}deg)"
>
</div>

<style>
  .box {
    /* Estilos estáticos aqui */
    border-radius: 8px;
  }
</style>

Ou com CSS Variables

<script>
  export let size = 100
  export let color = 'red'
  export let rotation = 0
</script>

<div
  class="box"
  style="--size: {size}px; --color: {color}; --rotation: {rotation}deg"
>
</div>

<style>
  .box {
    width: var(--size);
    background: var(--color);
    transform: rotate(var(--rotation));
    border-radius: 8px;
  }
</style>

Classes Condicionais

React

// Manual
<div className={`card ${isActive ? 'active' : ''} ${isHighlighted ? 'highlighted' : ''}`}>

// Com clsx/classnames
import clsx from 'clsx'
<div className={clsx('card', { active: isActive, highlighted: isHighlighted })}>

// styled-components
const Card = styled.div`
  /* base */
  ${props => props.active && css`/* active styles */`}
  ${props => props.highlighted && css`/* highlighted styles */`}
`

Svelte

<script>
  export let isActive = false
  export let isHighlighted = false
</script>

<!-- Diretiva class: -->
<div
  class="card"
  class:active={isActive}
  class:highlighted={isHighlighted}
>
</div>

<!-- Shorthand quando nome da classe = nome da variável -->
<div class="card" class:active class:highlighted>
</div>

<style>
  .card {
    padding: 1rem;
    border: 1px solid #ccc;
  }

  .active {
    border-color: blue;
  }

  .highlighted {
    background: yellow;
  }
</style>

Estilos Globais

Svelte: :global()

<style>
  /* Escopado normalmente */
  .container {
    padding: 1rem;
  }

  /* Global: afeta elementos filhos de qualquer componente */
  .container :global(h1) {
    color: red;
  }

  /* Totalmente global */
  :global(body) {
    margin: 0;
    font-family: system-ui;
  }

  /* Classes globais */
  :global(.error) {
    color: red;
  }
</style>

Arquivo Global

<!-- +layout.svelte ou App.svelte -->
<slot />

<style>
  /* Estilos globais da app */
  :global(*) {
    box-sizing: border-box;
  }

  :global(body) {
    margin: 0;
    font-family: 'Inter', sans-serif;
  }

  :global(a) {
    color: inherit;
    text-decoration: none;
  }
</style>

Ou arquivo CSS separado:

// main.js
import './global.css'
import App from './App.svelte'

CSS Variables como Props

Passando variáveis CSS para componentes

<!-- Box.svelte -->
<div class="box">
  <slot />
</div>

<style>
  .box {
    background: var(--bg, white);
    color: var(--color, black);
    padding: var(--padding, 1rem);
  }
</style>

<!-- Uso -->
<Box --bg="navy" --color="white" --padding="2rem">
  Conteúdo
</Box>

Isso compila para:

<div class="box svelte-xyz" style="--bg: navy; --color: white; --padding: 2rem;">
  Conteúdo
</div>

Tema Dinâmico

<!-- ThemeProvider.svelte -->
<script>
  export let theme = {
    primary: '#646cff',
    secondary: '#535bf2',
    background: '#242424',
    text: '#ffffff'
  }
</script>

<div
  class="theme"
  style:--primary={theme.primary}
  style:--secondary={theme.secondary}
  style:--bg={theme.background}
  style:--text={theme.text}
>
  <slot />
</div>

<style>
  .theme {
    background: var(--bg);
    color: var(--text);
  }
</style>

<!-- Qualquer componente filho -->
<style>
  button {
    background: var(--primary);
    color: var(--text);
  }

  button:hover {
    background: var(--secondary);
  }
</style>

Pré-processadores

Com vitePreprocess

// svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'

export default {
  preprocess: vitePreprocess()
}

SCSS

<style lang="scss">
  $primary: #646cff;
  $radius: 8px;

  .card {
    border-radius: $radius;

    &:hover {
      background: lighten($primary, 40%);
    }

    .title {
      color: $primary;
    }
  }
</style>

PostCSS / Tailwind

<!-- Com Tailwind configurado -->
<div class="flex items-center gap-4 p-4 bg-gray-100 rounded-lg">
  <img class="w-12 h-12 rounded-full" src={avatar} alt="" />
  <div>
    <h3 class="font-bold">{name}</h3>
    <p class="text-gray-600">{email}</p>
  </div>
</div>

<!-- Também funciona em <style> -->
<style>
  .custom {
    @apply flex items-center gap-4 p-4;
  }
</style>

Comparação Final

AspectoReact CSS-in-JSSvelte
SetupInstalar + configurarZero config
Runtime~10-20KB (styled-components)0KB
EscopoDepende da soluçãoAutomático
SintaxeTemplate literals / objetosCSS nativo
DinâmicoProps no JSCSS vars / style:
Classe condicionalclsx/classnamesclass: directive
GlobalThemeProvider:global()
ToolingPlugin específicoNative
DevToolsExtension específicaCSS padrão

✅ Desafio da Aula

Objetivo

Criar um componente Card com tema customizável via CSS variables.

Requisitos

  1. Componente Card.svelte com:

    • Props: variant (‘default’ | ‘primary’ | ‘danger’)
    • Slot para conteúdo
    • Slot nomeado “header”
    • Slot nomeado “footer”
  2. Cada variante tem cores diferentes via CSS vars

  3. Deve aceitar override de cores: <Card --bg="pink">

Spec de Verificação

  • Card default tem background neutro
  • Card primary tem background azul/roxo
  • Card danger tem background vermelho
  • Passar --bg="pink" sobrescreve a cor

Solução

🔍 Clique para ver a solução
<!-- Card.svelte -->
<script>
  export let variant = 'default'
</script>

<div class="card" class:primary={variant === 'primary'} class:danger={variant === 'danger'}>
  {#if $$slots.header}
    <div class="header">
      <slot name="header" />
    </div>
  {/if}

  <div class="body">
    <slot />
  </div>

  {#if $$slots.footer}
    <div class="footer">
      <slot name="footer" />
    </div>
  {/if}
</div>

<style>
  .card {
    --_bg: var(--bg, #f5f5f5);
    --_border: var(--border, #e0e0e0);
    --_text: var(--text, #333);

    background: var(--_bg);
    border: 1px solid var(--_border);
    color: var(--_text);
    border-radius: 8px;
    overflow: hidden;
  }

  .card.primary {
    --_bg: var(--bg, #646cff);
    --_border: var(--border, #535bf2);
    --_text: var(--text, white);
  }

  .card.danger {
    --_bg: var(--bg, #ef4444);
    --_border: var(--border, #dc2626);
    --_text: var(--text, white);
  }

  .header {
    padding: 1rem;
    border-bottom: 1px solid var(--_border);
    font-weight: 600;
  }

  .body {
    padding: 1rem;
  }

  .footer {
    padding: 1rem;
    border-top: 1px solid var(--_border);
    background: rgba(0, 0, 0, 0.05);
  }
</style>
<!-- Uso -->
<Card>
  <span slot="header">Título</span>
  <p>Conteúdo do card</p>
  <div slot="footer">
    <button>Ação</button>
  </div>
</Card>

<Card variant="primary">
  Conteúdo primário
</Card>

<Card variant="danger" --bg="pink">
  Card perigo com override de cor
</Card>

Próxima aula: 2.6 — Performance e Tamanho do Bundle