
Привет, друзья!
В данной статье мы разработаем простую, но относительно полноценную дизайн-систему для веб-приложения средствами Sass.
Почему Sass? Потому что, кроме полной поддержки CSS, Sass предоставляет несколько интересных инструментов, позволяющих существенно сократить шаблонный код, в чем вы сами скоро убедитесь. На мой взгляд, несмотря на стремительное развитие CSS в последние годы, Sass продолжает оставаться актуальным, по крайней мере, при работе над серьезными проектами.
При разработке дизайн-системы в части терминологии, названий, значений переменных и т.п. я буду ориентироваться, в основном, на Bootstrap и немного на Tailwind.
Если вам это интересно, прошу под кат.
Для работы с зависимостями будет использоваться Yarn.
Для тестирования дизайн-системы можно воспользоваться одним из готовых шаблонов приложений, предоставляемых Vite, например:
# создаем шаблон приложения # sass-style - название приложения и директории проекта # --template vanilla - используемый шаблон yarn create vite sass-style --template vanilla # переходим в созданную директорию и устанавливаем зависимости cd sass-style && yarn # устанавливаем `sass` в качестве зависимости для разработки yarn add -D sass # запускаем приложение в режиме разработки yarn dev
Создаем в корне проекта директорию styles и в ней — файл index.scss. Это основной файл стилей нашего приложения. Убедитесь, что он импортируется в файле main.js:
import "./styles/index.scss";
Сброс стилей
Реализуем минимальный сброс стилей.
Создаем директорию styles/utils и в ней — файл _reset.scss следующего содержания:
*, *::before, *::after { box-sizing: border-box; } body, h1, h2, h3, h4, h5, p, figure, picture { margin: 0; } h1, h2, h3, h4, h5, h6, p { font-weight: 400; } img, picture { display: block; max-width: 100%; } input, button, textarea, select { font: inherit; } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: 0.01ms !important; } }
Импортируем этот файл в index.scss и определяем еще парочку дефолтных стилей для html и body:
@import "./utils/reset"; html { // о переменных см. ниже font-size: $fs-base; } body { font-family: Helvetica, sans-serif; color: $dark; line-height: 1.5; }
Переменные / Variables
Создаем файл styles/_vars.scss и определяем в нем несколько переменных:
// минимальная и максимальная ширина области просмотра $min-w: 375px; $max-w: 1536px; // отступы $spacer-base: 30px; $spacer-xxs: 0.25 * $spacer-base; $spacer-xs: 0.5 * $spacer-base; $spacer-s: 0.75 * $spacer-base; $spacer-m: $spacer-base; $spacer-l: 1.5 * $spacer-base; $spacer-xl: 2 * $spacer-base; $spacer-xxl: 2.25 * $spacer-base; // шрифты $fs-base: 16px; $fs-xxs: 11px; $fs-xs: 12px; $fs-s: 14px; $fs-m: $fs-base; $fs-l: 22px; $fs-xl: 24px; $fs-xxl: 26px; // цвета // bootstrap $primary: #0275d8; $success: #5cb85c; $info: #5bc0de; $warning: #f0ad4e; $danger: #d9534f; $light: #f7f7f7; $dark: #292b2c; // дополнительно $gray100: #fefefe; $gray200: #fbfbfb; $gray300: #fafafa; $gray400: #f3f3f3; $gray500: #eeeeee; $gray600: #a3a3a3; $gray700: #8c8c8c; $gray800: #555555; $gray900: #333333; $white: #fff; $black: #000; // контрольные точки // bootstrap $sm: 576px; $md: 768px; $lg: 992px; $xl: 1200px; $xxl: 1400px;
Импортируем этот файл в начале index.scss:
@import "./vars";
Это делает переменные доступными глобально — переменные будут доступны всем импортируемым в index.scss модулям.
Подробнее о переменных Sass можно почитать здесь.
Отступы
Создаем файл utils/_spacing.scss следующего содержания:
// перечисление отступов $spacers: ( "0": 0, xxs: $spacer-xxs, xs: $spacer-xs, s: $spacer-s, m: $spacer-m, l: $spacer-l, xl: $spacer-xl, xxl: $spacer-xxl ); // типы отступов $types: ( "m": "margin", "p": "padding" ); // стороны $sides: ( "": "", t: "-top", r: "-right", b: "-bottom", l: "-left" ); // перебираем перечисление @each $key-spacer, $factor in $spacers { // перебираем типы @each $key-type, $type in $types { // перебираем стороны @each $key-side, $side in $sides { // для всех и каждой стороны .#{$key-type}#{$key-side}-#{$key-spacer} { #{$type}#{$side}: $factor; } } // для горизонтального отступа .#{$key-type}x-#{$key-spacer} { #{$type}-left: $factor; #{$type}-right: $factor; } // для вертикального отступа .#{$key-type}y-#{$key-spacer} { #{$type}-bottom: $factor; #{$type}-top: $factor; } } // дополнительно // пространство между гридами или флексами .gap-#{$key-spacer} { gap: $factor; } }
О директиве @each можно почитать здесь.
Это позволяет определять внешние (margin) и внутренние (padding) отступы следующим образом:
mилиp(в дальнейшем паддинг предполагается) — для отступов по всем сторонам, внешним или внутренним, соответственно. Например,m-xsкомпилируется в:
margin-top: 15px; margin-right: 15px; margin-bottom: 15px; margin-left: 15px;
mx/my— для отступов по горизонтали/вертикали. Например,mx-sкомпилируется в:
margin-right: 15px; margin-left: 15px;
mt/mr/mb/ml— для отступов с соответствующей стороны. Например,mt-sкомпилируется в:
margin-top: 15px;
Цвета
Создаем файл utils/_colors.scss следующего содержания:
// перечисление цветов $colors: ( 'gray100': $gray100, 'gray200': $gray200, 'gray300': $gray300, 'gray400': $gray400, 'gray500': $gray500, 'gray600': $gray600, 'gray700': $gray700, 'gray800': $gray800, 'gray900': $gray900, 'primary': $primary, 'success': $success, 'info': $info, 'warning': $warning, 'danger': $danger, 'light': $light, 'dark': $dark, 'white': $white, 'black': $black ); // перебираем перечисление @each $name, $color in $colors { // цвет текста .color-#{$name} { color: $color; } // цвет фона .bg-#{$name} { background-color: $color; } // заливка `svg` .fill-#{$name} { svg { color: $color; } } }
Это позволяет определять цвет текста, фона или заливки для SVG. Например, color-primary компилируется в:
color: #0275d8;
bg-success в:
background-color: #5cb85c;
Размеры шрифтов и блоков
Создаем файл utils/_font.scss следующего содержания:
// перечисление размеров шрифта $font-sizes: ( "0": 0, xxs: $fs-xxs, xs: $fs-xs, s: $fs-s, m: $fs-m, l: $fs-l, xl: $fs-xl, xxl: $fs-xxl ); // перебираем перечисление @each $size, $factor in $font-sizes { .fs-#{$size} { font-size: $factor; } } // перебираем возможные значения свойства `font-weight` @each $v in 100, 200, 300, 400, 500, 600, 700, 800, 900 { .fw-#{$v} { font-weight: $v; } }
fs-s компилируется в:
font-size: 14px;
fw-600 в:
font-weight: 600;
Создаем файл utils/_sizing.scss следующего содержания:
@each $v in 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 { .w-#{$v} { width: calc(100% / 12 * $v); } .h-#{$v} { height: calc(100% / 12 * $v); } } // дополнительно .w-full { width: 100vw; } .h-full { height: 100vh; }
Это позволяет распределять ширину и высоту блоков по сетке, состоящей из 12 равных частей. Об этом можно думать так: сколько колонок/строк должен занимать блок? Например, w-4 компилируется в:
width: 33.3333333333%;
h-2 в:
height: 16.6666666667%;
Пример:
<div style="display: flex" class="w-full h-full"> <div class="w-2 h-4 bg-primary"></div> <div class="w-6 h-4 bg-success"></div> <div class="w-4 h-4 bg-danger"></div> </div>
Результат:

Флекс
Создаем файл utils/_flex.scss следующего содержания:
// justify-content @each $v in start, end, center, stretch, between, around, evenly { .justify-#{$v} { @if ($v == start) or ($v == end) { justify-content: flex-#{$v}; } @else if ($v == between) or ($v == around) or ($v == evenly) { justify-content: space-#{$v}; } @else { justify-content: $v; } } } // align-items @each $v in start, end, center, stretch { .items-#{$v} { @if ($v == start) or ($v == end) { align-items: flex-#{$v}; } @else { align-items: $v; } } } // дополнительно .flex-center { // о миксинах см. ниже @include flex-center(); } .flex-center-column { @include flex-center(column); } .flex-wrap { flex-wrap: wrap; } .flex { flex: 1; }
При желании, таким же способом можно определить justify-items, align-content, justify-self и align-self.
Утилиты
Создаем файл utils/_utils.scss следующего содержания:
// display @each $v in none, inline, block, inline-block, flex, inline-flex, grid, inline-grid { .d-#{$v} { display: $v; } } // position @each $v in relative, absolute, fixed, sticky { .p-#{$v} { position: $v; } } // контент для читалок // visually-hidden .sr-only { background: none; border: none; color: none; cursor: none; height: 0; margin: 0; opacity: 0; outline: none; overflow: hidden; padding: 0; pointer-events: none; position: absolute; user-select: none; visibility: hidden; white-space: nowrap; width: 0; z-index: -1; } // дополнительно .text-overflow { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .text-center { text-align: center; } // ваши классы-утилиты
Перепишем последний пример:
<div class="w-full h-full d-flex gap-xxs"> <div class="w-2 h-4 bg-primary flex-center"> <p class="fs-xl color-success">box#1</p> </div> <div class="w-6 h-4 bg-success d-flex justify-end items-end"> <p class="pr-s fs-xl color-primary">box#2</p> </div> <div class="w-4 h-4 bg-danger d-flex items-end"> <p class="pl-s fs-xl color-gray300">box#3</p> </div> </div>
Результат:

Не забываем импортировать все утилиты в index.scss.
На этом с основами дизайн-системы мы закончили. Остальное зависит от потребностей конкретного приложения и личных предпочтений разработчика.
Бонус
Функции / Functions
В числе прочего, Sass позволяет создавать функции для выполнения операций над значениями свойств CSS.
Определим функцию для преобразования px в rem.
Создаем файл styles/_fns.scss следующего содержания:
@function strip-unit($v) { @return $v / ($v * 0 + 1); } @function rem($v) { @return #{strip-unit($v / $fs-base)} + rem; }
Функция strip-unit позволяет игнорировать px в передаваемом функции значении.
Пример использования функции rem:
p { font-size: rem(16); // или font-size: rem(16px); // или font-size: rem($fs-m); } // после компиляции p { font-size: 0.8888888889rem }
Подробнее о функциях можно почитать здесь.
Миксины / Mixins
Миксин — это блок готовых стилей, которые включаются в другие стили.
Миксин определяется с помощью директивы @mixin и внедряется с помощью директивы @include.
Создаем файл styles/_mixins.scss.
Определяем миксин для выравнивания элемента по центру с помощью display: flex:
@mixin flex-center($d: row) { display: flex; justify-content: center; align-items: center; @if ($d == column) { flex-direction: column; } }
Мы видели пример использования данного миксина в utils/_flex.scss.
О директивах @if и @else можно почитать здесь.
Определяем миксин для стилизации плейсхолдера:
@mixin placeholder { &.placeholder { @content; } &:-moz-placeholder { @content; } &::-moz-placeholder { @content; } &:-ms-input-placeholder { @content; } &::-webkit-input-placeholder { @content; } }
Директива-переменная @content содержит стили, передаваемые миксину.
Пример использования:
input { @include placeholder { color: $primary; font-size: 0.8rem; opacity: 0.8; } }
Результат после компиляции:
input.placeholder { color: #0275d8; font-size: 0.8rem; opacity: 0.8; } input:-moz-placeholder { color: #0275d8; font-size: 0.8rem; opacity: 0.8; } input::-moz-placeholder { color: #0275d8; font-size: 0.8rem; opacity: 0.8; } input:-ms-input-placeholder { color: #0275d8; font-size: 0.8rem; opacity: 0.8; } input::-webkit-input-placeholder { color: #0275d8; font-size: 0.8rem; opacity: 0.8; }
Определя��м миксин для стилизации прокрутки:
@mixin scrollbar($scrollbar-width, $thumb-color, $track-color) { &::-webkit-scrollbar { width: #{$scrollbar-width} + px; } &::-webkit-scrollbar-track { background-color: $track-color; } &::-webkit-scrollbar-thumb { background-color: $thumb-color; } // firefox & { @if ($scrollbar-width == 0) { scrollbar-width: none; // приблизительное значение } @else if ($scrollbar-width < 19) { scrollbar-width: thin; } @else { scrollbar-width: auto; } scrollbar-color: $thumb-color $track-color; } }
Пример использования:
@use "sass:color"; html { @include scrollbar(12, lighten($primary, 25), $gray500); }
Функция lighten из встроенного модуля sass:color позволяет осветлять цвета.
Определяем миксины — альтернативу медиа-запросам:
// перечисление контрольных точек $breakpoints: ( "sm": $sm, "md": $md, "lg": $lg, "xl": $xl, "xxl": $xxl ); // от указанной точки и шире @mixin up($b) { @if map-has-key($breakpoints, $b) { $v: map-get($breakpoints, $b); @media (min-width: $v) { @content; } } } // от указанной точки и уже @mixin down($b) { @if map-has-key($breakpoints, $b) { $v: map-get($breakpoints, $b); @media (max-width: $v) { @content; } } }
Пример использования up:
h1 { font-size: rem(18); // > 768px @include up(md) { font-size: rem(24); font-weight: bold; } // > 1200px @include up(xl) { font-size: rem(30); } }
В качестве последнего упражнения — миксин для изменения размера шрифта пропорционально изменению ширины области просмотра:
@mixin fluid-fs($min-w, $max-w, $min-fs, $max-fs) { $u1: unit($min-w); $u2: unit($max-w); $u3: unit($min-fs); $u4: unit($max-fs); @if $u1 == $u2 and $u1 == $u3 and $u1 == $u4 { & { font-size: $min-fs; @media (min-width: $min-w) { font-size: calc( #{$min-fs} + #{strip-unit($max-fs - $min-fs)} * ((100vw - #{$min-w}) / #{strip-unit($max-w - $min-w)}) ); } @media (min-width: $max-w) { font-size: $max-fs; } } } }
Пример использования:
html { @include fluid-fs($min-w, $max-w, $fs-xs, $fs-xl); }
При изменении ширины области просмотра размер шрифта элемента html (:root) будет пропорционально меняться от 12 до 24px. В случае применения единиц измерения rem, размер шрифта вычисляется от базового, т.е. размера шрифта html. Таким образом, заранее позаботившись об установке размеров в rem или применении одноименной функции, можно автоматически получить гибкую систему шрифтов во всем приложении.
Пожалуй, это все, чем я хотел поделиться с вами в данной статье. Надеюсь, вы узнали для себя что-то новое и не зря потратили время.
Благодарю за внимание и happy coding!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

