Не так давно я смотрел видео тренировочного собеседования фронтенд-разработчика на React, которому задавались вопросы уровня middle и выше. Вторая часть вопросов, после знакомства, касалась стилизации - как с помощью CSS сделать ту или иную вещь. Интересно, что собеседуемый «плавал» в свойствах и правилах CSS, и даже синтаксисе, что он объяснял тем, что все последние годы он работал с CSS-in-JS и как оно там в нативном CSS он помнит плохо. К слову, JavaScript тот парень знает хорошо, и, возможно, это нормально, когда человек плохо знает как прописать стили элементу, если вся его разработка - это сплошной JS.

Интересно еще и то, что сам React насчет стилизации особо не переживает. В документации этой библиотеки сказано, что у него нет своего мнения о том, как должны определяться стили. То есть делайте со своими дизайнами, что хотите. Кто знает, может быть поэтому у Facebook (запрещен на территории РФ) такой уродливый дизайн ;-) В свою очередь создатель Svelte Рич Харрис заявлял в свое время что фреймворк, не имеющий встроенного пути для добавления стилей в компонент, является незаконченным фреймворком. Спорить тут не будем, просто констатируем факт - у Svelte есть свои правила работы со стилями и именно о них мы и поговорим.

Внешний и внутренний

Начнем с того, что в Svelte вы можете создать внешний файл стилей и подключить его сразу ко всему сайту. Или создать под каждый компонент свой файл стилей и импортировать его только туда, то есть использовать практически модульную систему. Это полезно для использования методов reset.css или normalize.css, подключения шрифтов, задания глобальных переменных и других глобальных стилей. Такие стили можно подключить импортом в свой файл +layout.svelte в папке routes, который как правило хранит в себе код скелета вашего проекта. Размещать глобальные стили можно в отдельном файле или файлах в папке src/lib.

src/routes/+layout.svelte
<script>
    import Header from '$lib/components/Header.svelte'
    import Footer from '$lib/components/Footer.svelte'
    import '$lib/styles/style.css'
</script>

<Header/>

<main>
    <slot />
</main>

<Footer/>

Для отдельных компонентов делать отдельные файлы CSS нет смысла, так как Svelte предлагает простой механизм использования стилей внутри самого файла компонента. И делается это как в банальном HTML: стили в компоненте прописываются внутри тега <style>.

src/lib/components/Counter.svelte
<script>
	let count = 0
	const increment = () => (count += 1)
</script>

<style>
    button {
        background: blue;
        border: 2px solid var(--color-primary);
        border-radius: 5px;
        font-size: 1rem;
    }
</style>

<button on:click={increment}>
	{count}
</button>

Пост и пре

К Svelte можно подключить ваш любимый препроцессор, а также PostCSS и использовать их синтаксис не только в отдельных файлах, но и внутри компонентов.

src/lib/components/Counter.svelte
<style type="text/scss">
    button {
        background: blue;
        border: 2px solid var(--color-primary);
        border-radius: 5px;

        span {
            font-size: $font;
        }
    }
</style>

А если вы жить не можете без Styled components или Emotion, или, например, Tailwind, подключите их и используйте. О том, как подключать все это и использовать нужно будет написать отдельный пост.

То есть со Svelte у вас тоже будет свобода, как с React, с той лишь разницей, что если вам все эти прослойки между стилями в разработке и стилями в браузере не нужны, вы можете их полностью исключить из своей работы.

Не дальше компонента

Написание стилей в компонентах Svelte хорошо тем, что область действия этих стилей дальше самого компонента не распространяется. Это позволяет не ломать голову над придумыванием классов, так как даже элементы с одинаковыми классами в разных компонентах не получат стили друг друга. Я могу просто обращаться к тегу, если такой тег с таким стилем на странице будет один. К примеру, набор правил для <button>, который вы задали соответствующему элементу в компоненте, будет применен только к нему, а не к каким—либо другим HTML-элементам с тегом <button> на странице. О классах можно будет вспомнить, если у вас есть несколько кнопок внутри компонента и вы хотели бы оформить их по-разному. Классы также будут иметь ограниченную область действия.

Это работает благодаря тому, что Svelte сам генерирует имена классов. Они выглядят как тарабарщина, потому что состоят из набора случайных цифр и букв. И да, возможно читать такой код в браузере будет сложно, зато тратить львиную долю своего времени на обдумывание и написание классов по методологии БЭМ вам на придется, так как и сами имена классов с подходом Svelte можно упростить. Также можно и спокойно использовать наши служебные классы и переменные, которые мы определяли ранее в нашем глобальном стилевом файле.

src/lib/components/Spoiler.svelte
<script>
    export let summary = 'Если короче'
</script>

<details>
    <summary>{summary}</summary>
    <slot />
</details>

<style>
    details {
        box-shadow: 0px 0px 0px var(--box-shadow-color);
    }

    details:hover {
        box-shadow: 5px 5px 0px var(--box-shadow-color)
    }

    details[open]:hover {
        box-shadow: 0px 0px 0px var(--box-shadow-color);
    }

    details > summary {
        cursor: pointer;
        background: var(--color-accent);
        display: flex;
        align-items: center;
        line-height: 1;
        font-weight: 900;
    }

    details[open] > summary {
        background: var(--color-secondary);
    }

    @media(min-width: 992px) {
        summary:before {
            margin-right: var(--unit);
        }
    }
</style>

Обратите внимание, что фрагмент кода выше почти один в один повторяет один из компонентов на этом сайте. То есть ситуации, когда нам в принципе не нужны классы, а значит всевозможные методологии нейминга элементов, вроде БЭМ, абсолютно реальны. Вы можете создать второй такой же компонент и задать тем же тегам другие свойства, и у вас все будет работать как надо. А если вы хотите, чтобы тот же тег details, к примеру, получил какой-то глобальный стиль, который был бы доступен по всему проекту, то можно использовать специальную директиву :global.

src/lib/components/Spoiler.svelte
<style>
    /* ... */
    :global(details) {
        padding: 10px 15px;
    }
    :global(.btn) {
        background: #000;
    }
    :global(summary) > p {
        font-size: 1.5rem;
    }
</style>

Это очень полезно, когда на странице, которую вы стилизуете, нет элементов, которые вам хотелось бы стилизовать. Например, если у вас есть компонент модального окна, внутри которого нет разметки с тегом body или html, но задать им какой-нибудь стиль надо, вы можете использовать атрибут :global, вставим его перед конкретным правилом, либо задать его для всего вашего блока стилей в компоненте, если вам нужно чтобы все правила в нем стали глобальными. Только увлекаться этим не стоит, так как есть риск получить перезаписываемые стили.

+page.svelte
<!-- блок с полностью глобальными стилями -->
<style global>
  :global(.prose) :global(h1) {
    color: aqua;
  }
</style>

Это важно:

  • Компилятор Svelte предупредит вас, что стиль, который вы прописали элементу в компоненте, нигде в его разметке не присутствует.
  • Классы с «абракадаброй» будут добавлены только к тем элементам, которым вы задали стили. Это немного сократит общую массу кода.
  • Директива :global сработает только если использовать ее перед или после класса/тега, которым вы задаете стили. Вот так .prose :global(p) > span {/***/} не сработает.

В динамике

Со стилями вроде разобрались. Давайте теперь вернемся к классам. Фреймворки хороши тем, что позволяют задавать классы различным блокам динамически, не используя ванильное обращение скрипта к DOM. Svelte в этом случае не исключение. Вот как лаконично и красиво можно задать элементу классы, определённые ранее в блоке скриптов:

Button.svelte
<script>
  export let big = false;
  export let ghost = false;
</script>

<style>
  .big {
    font-size: 20px;
    display: block;
    width: 100%;
  }

  .ghost {
    background-color: transparent;
    border: solid currentColor 2px;
  }
</style>

<button class:big class:ghost>
  <slot/>
</button>

С помощью такого простого кода можно создать, к примеру, компонент и при вызове этого компонента указать нужный пропс, чтобы применился нужный класс и соответствующие стили.

+page.svelte
<script>
  import Button from './Button.svelte';
</script>

<Button big ghost>Click Me</Button>

Чтобы не прописывать повторяющиеся стили элементу с разными классами, можно задать ему класс без использования пропсов, где будут прописаны стили, характерные для элементов с обоими динамическими классами. Этот класс всегда будет присутствовать у элемента, какие бы дополнительные классы вы ему не задавали.

Button.svelte
<script>
  export let big = false;
  export let ghost = false;
</script>

<style>
    .button {
        padding: 10px 20px;
        border-radius: 6px;
    }

    .big {
        font-size: 20px;
        display: block;
        width: 100%;
    }

    .ghost {
        background-color: transparent;
        border: solid currentColor 2px;
    }

</style>

<button class="button" class:big class:ghost>
  <slot/>
</button>

Если вы хотите задать класс элементу не в файле компонента, а в том файле, куда он импортируется, то можно сделать и такое

Button.svelte
<script>
    let class_name = '';
    export { class_name as class };
</script>

<button class="c-btn {class_name}">
  <slot />
</button>

Вот пример использования динамической смены класса у элемента в зависимости от ситуации:

Component.svelte
<div class="primary" class:error={valid == false}>{message}</div>

Класс primary во фрагменте кода выше будет добавлен в любом случае, а вот класс error будет добавлен только если значение valid будет равно false. То же самое, но с вариантом, можно записать в другом виде. Это тоже будет работать:

Component.svelte
<div class="primary" class="{valid == false ? 'error' : 'valid'}">{message}</div>

Если вас интересует только класс valid, который бы соответствовал имени переменной, то записать код можно так:

Component.svelte
<div class:valid>{message}</div>

Очень коротко, правда? Да еще и работает как надо.

Svelte позволяет также использовать встроенные в разметку стили. При этом можно сильно «наскриптить» в начале, чтобы, например, рандомизировать какие-нибудь свойства.

SocialCard.svelte
<script>
    const backgrounds = [
        {
            backgroundImage: 'radial-gradient(#F7C90D 15%, rgba(0,0,0,0) 16%), radial-gradient(#ED6F35 15%, rgba(0,0,0,0) 16%)',
            backgroundColor: '#fff',
            backgroundPosition: '0 0, 30px 30px',
            backgroundSize: '60px 60px',
        },
        {
            backgroundImage: 'radial-gradient(#64BAAA 20%, #ffffff00 0%), radial-gradient(#F7C90D 20%, #ffffff00 0%)',
            backgroundColor: '#eee',
            backgroundSize: '20px 20px',
            backgroundPosition: '0 0, 10px 10px'
        }
]

let randomBg = backgrounds[Math.floor(Math.random() * backgrounds.length)];
</script>

<div
    style="display: flex;
        flex-direction: column;
        height: 100%;
        width: 100%;
        color: #292929;
        background-color:{`${randomBg.backgroundColor}`};
        background: {`${randomBg.backgroundImage}`};
        background-repeat: {`${randomBg.backgroundRepeat}`};
        background-size: {`${randomBg.backgroundSize}`};
        background-position: {`${randomBg.backgroundPosition}`};"
>
    Название статьи
</div>

Этот код кстати с данного сайта и используется для динамического создания картинок, которые будут видны, если вы захотите поделиться этой статьей.

Как видите, вариантов использования CSS в Svelte предостаточно, в том числе и в ситуации, когда классы и свойства, нужно назначить динамически.

Но есть одно «но»

Читаешь, и, наверное, думаешь, как же все с CSS в Svelte круто и весело, но все ли так гладко со стилизацией в этом фреймворке, как описано в этой статье? На самом деле, почти гладко. Вот, например, код который не будет работать.

+page.svelte
<script>
  export let cols = 4;
</script>

<style>
    ul {
        display: grid;
        width: 100%;
        grid-column-gap: 16px;
        grid-row-gap: 16px;
        grid-template-columns: repeat({cols}, 1fr);
    }
</style>

<ul>
    <slot />
</ul>

Здесь мы решили с вами создать классный компонент с разным количеством колонок, в зависимости от того, куда мы его будем импортировать. Создаем пропс со значением по умолчанию и записываем его в свойство grid-template-columns. В подходе с CSS-in-JS примерное такое бы сработало, но не в Svelte. Дело в том, что компилятор поддерживает синтаксис CSS практически в ванильном виде (из него правда выбивается только директива :global), а такого рода переменные, как в коде выше, в блоке style не поддерживаются.

Однако выход из этой ситуации есть. Нам на помощь приходят возможности нашего ванильного CSS c пользовательскими переменными.

+page.svelte
<script>
    export let cols = 4;
</script>

<style>
    ul {
        display: grid;
        width: 100%;
        grid-column-gap: 16px;
        grid-row-gap: 16px;
        grid-template-columns: repeat(var(--columns), 1fr);
    }
</style>

<ul style="--columns:{cols}">
    <slot />
</ul>

Здесь мы используем тот же пропс, но в стилях вместо него, прописываем CSS-переменную, которую в свою очередь вместе с пропсом в качестве значения указываем во встроенном стиле элемента. И теперь вызывая в другом месте наш компонент можем просто указать ему количество колонок <List col="3" />, отличающееся от значения по умолчанию, и все будет работать. CSS как вы скорее всего знаете позволяет задавать переменную в инлайновом варианте стилизации непосредственно тому элементу, кому эта переменная предназначается. Поэтому все работает.

Это все, что я хотел сказать по поводу подхода к стилизации в Svelte. Если у вас есть какие-нибудь дополнения, то напишите мне в телеграме, или на странице с контактами.