Подход Svelte к CSS
Опубликовано 17 февр. 2024 г.
Не так давно я смотрел видео тренировочного собеседования фронтенд-разработчика на React, которому задавались вопросы уровня middle и выше. Вторая часть вопросов, после знакомства, касалась стилизации - как с помощью CSS сделать ту или иную вещь. Интересно, что собеседуемый «плавал» в свойствах и правилах CSS, и даже синтаксисе, что он объяснял тем, что все последние годы он работал с CSS-in-JS и как оно там в нативном CSS он помнит плохо. К слову, JavaScript тот парень знает хорошо, и, возможно, это нормально, когда человек плохо знает как прописать стили элементу, если вся его разработка - это сплошной JS.
Интересно еще и то, что сам React насчет стилизации особо не переживает. В документации этой библиотеки сказано, что у него нет своего мнения о том, как должны определяться стили. То есть делайте со своими дизайнами, что хотите. Кто знает, может быть поэтому у Facebook (запрещен на территории РФ) такой уродливый дизайн ;-) В свою очередь создатель Svelte Рич Харрис заявлял в свое время что фреймворк, не имеющий встроенного пути для добавления стилей в компонент, является незаконченным фреймворком. Спорить тут не будем, просто констатируем факт - у Svelte есть свои правила работы со стилями и именно о них мы и поговорим.
Внешний и внутренний
Начнем с того, что в Svelte вы можете создать внешний файл стилей и подключить его сразу ко всему сайту. Или создать под каждый компонент свой файл стилей и импортировать его только туда, то есть использовать практически модульную систему. Это полезно для использования методов reset.css
или normalize.css
, подключения шрифтов, задания глобальных переменных и других глобальных стилей. Такие стили можно подключить импортом в свой файл +layout.svelte
в папке routes
, который как правило хранит в себе код скелета вашего проекта. Размещать глобальные стили можно в отдельном файле или файлах в папке src/lib
.
<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>
.
<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 и использовать их синтаксис не только в отдельных файлах, но и внутри компонентов.
<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 можно упростить. Также можно и спокойно использовать наши служебные классы и переменные, которые мы определяли ранее в нашем глобальном стилевом файле.
<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
.
<style>
/* ... */
:global(details) {
padding: 10px 15px;
}
:global(.btn) {
background: #000;
}
:global(summary) > p {
font-size: 1.5rem;
}
</style>
Это очень полезно, когда на странице, которую вы стилизуете, нет элементов, которые вам хотелось бы стилизовать. Например, если у вас есть компонент модального окна, внутри которого нет разметки с тегом body
или html
, но задать им какой-нибудь стиль надо, вы можете использовать атрибут :global
, вставим его перед конкретным правилом, либо задать его для всего вашего блока стилей в компоненте, если вам нужно чтобы все правила в нем стали глобальными. Только увлекаться этим не стоит, так как есть риск получить перезаписываемые стили.
<!-- блок с полностью глобальными стилями -->
<style global>
:global(.prose) :global(h1) {
color: aqua;
}
</style>
Это важно:
- Компилятор Svelte предупредит вас, что стиль, который вы прописали элементу в компоненте, нигде в его разметке не присутствует.
- Классы с «абракадаброй» будут добавлены только к тем элементам, которым вы задали стили. Это немного сократит общую массу кода.
- Директива
:global
сработает только если использовать ее перед или после класса/тега, которым вы задаете стили. Вот так.prose :global(p) > span {/***/}
не сработает.
В динамике
Со стилями вроде разобрались. Давайте теперь вернемся к классам. Фреймворки хороши тем, что позволяют задавать классы различным блокам динамически, не используя ванильное обращение скрипта к DOM. 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>
С помощью такого простого кода можно создать, к примеру, компонент и при вызове этого компонента указать нужный пропс, чтобы применился нужный класс и соответствующие стили.
<script>
import Button from './Button.svelte';
</script>
<Button big ghost>Click Me</Button>
Чтобы не прописывать повторяющиеся стили элементу с разными классами, можно задать ему класс без использования пропсов, где будут прописаны стили, характерные для элементов с обоими динамическими классами. Этот класс всегда будет присутствовать у элемента, какие бы дополнительные классы вы ему не задавали.
<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>
Если вы хотите задать класс элементу не в файле компонента, а в том файле, куда он импортируется, то можно сделать и такое
<script>
let class_name = '';
export { class_name as class };
</script>
<button class="c-btn {class_name}">
<slot />
</button>
Вот пример использования динамической смены класса у элемента в зависимости от ситуации:
<div class="primary" class:error={valid == false}>{message}</div>
Класс primary
во фрагменте кода выше будет добавлен в любом случае, а вот класс error
будет добавлен только если значение valid
будет равно false
. То же самое, но с вариантом, можно записать в другом виде. Это тоже будет работать:
<div class="primary" class="{valid == false ? 'error' : 'valid'}">{message}</div>
Если вас интересует только класс valid
, который бы соответствовал имени переменной, то записать код можно так:
<div class:valid>{message}</div>
Очень коротко, правда? Да еще и работает как надо.
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 круто и весело, но все ли так гладко со стилизацией в этом фреймворке, как описано в этой статье? На самом деле, почти гладко. Вот, например, код который не будет работать.
<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 пользовательскими переменными.
<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. Если у вас есть какие-нибудь дополнения, то напишите мне в телеграме, или на странице с контактами.