
С самого начала истории интернета мы нуждались в стилях для наших сайтов. Многие годы нам для этого служил CSS, развивавшийся в своём темпе. И здесь мы рассмотрим историю его развития.
Думаю, все согласятся с таким определением: CSS используется для описания представления документа, написанного на языке разметки. Также ни для кого не будет новостью, что за время развития CSS стал довольно мощным средством и что для использования в команде нужны дополнительные инструменты.
Дикий CSS
В 1990-е мы увлекались созданием «обалденных» интерфейсов, wow-фактор был самым важным. В те времена ценились inline-стили, и нас не заботило, если какие-то элементы страницы выглядели по-разному. Веб-страницы были милыми игрушками, которые мы насыщали прикольными гифками, бегущими строками и прочими кошмарными (но впечатляющими) элементами, стараясь привлечь внимание посетителей.
Затем мы начали создавать динамические сайты, но CSS оставался оплотом беспредела: каждый разработчик имел собственное представление, как делать CSS. Кто-то боролся со специфичностью (specificity), приводившей к визуальной регрессии при появлении нового кода. Мы полагались на !important, тем самым желая высечь в камне символ нашей воли к тому, чтобы элементы интерфейса выглядели определённым образом. Но вскоре мы поняли:

С увеличением размеров и сложности проектов, а также разрастанием команд разработчиков все эти методики превращались во всё более очевидные и крупные проблемы. Поэтому отсутствие закономерностей в применении стилей стало одним из главных препятствий для опытных и неопытных разработчиков, старавшихся найти правильный способ использования CSS. В конце концов мы поняли, что не существует правильных и неправильных способов. Мы лишь старались сделать так, чтобы всё выглядело прилично.

SASS спешит на помощь
SASS превратил CSS в приличный язык программирования, представленный в виде препроцессингового движка, реализующего в таблицах стилей вложенность, переменные, миксины, расширения (extends) и логику. Так что вы можете лучше организовать свои CSS-файлы, и вам доступны несколько способов разложения больших кусков CSS-кода по более мелким файлам. В своё время это стало прекрасным нововведением.
Принцип такой: берётся CSS-код, предварительно обрабатывается, и в общий CSS-пакет помещается скомпилированный файл. Круто? На самом деле не слишком. Через некоторое время стало понятно, что без стратегий и применения лучших методик SASS приносит больше проблем, чем решает.
Внезапно разработчики перестали вникать в то, что именно делает препроцессор, и начали лениво полагаться на вложенность ради победы над специфичностью. Но это привело к резкому увеличению размеров скомпилированных страниц стилей.
Пока не появился BEM…
BEM и концепция компонентов
BEM стал глотком свежего воздуха. Он позволил нам больше думать о возможности многократного использования и компонентах. По сути, эта технология вывела семантику на новый уровень. Теперь мы могли быть уверены, что className — уникален и что за счёт использования простого соглашения Block, Element, Modifier снижается риск специфического отображения.
Взгляните на пример:
<body class="scenery"> <section class="scenery__sky"> <div class="sky [sky--dusk / sky--daytime] [sky--foggy]"> <div class="sky__clouds"></div> <div class="sky__sun"></div> </div> </section> <section class="scenery__ground"></section> <section class="scenery__people"></section> </body>
Если вы проанализируете разметку, то сразу увидите работу соглашения BEM. В коде есть два явных блока:
.scenery и .sky. Каждый из них имеет собственные блоки. Лишь у sky есть модификаторы, потому что, к примеру, туман, день или закат — всё это разные характеристики, которые могут применяться к одному и тому же элементу.Для лучшего анализа взглянем на сопровождающий CSS, содержащий некий псевдокод:
// Block .scenery { //Elements &__sky { fill: screen; } &__ground { float: bottom; } &__people { float: center; } } //Block .sky { background: dusk; // Elements &__clouds { type: distant; } &__sun { strength: .025; } // Modifiers &--dusk { background: dusk; .sky__clouds { type: distant; } .sky__sun { strength: .025; } } &--daytime { background: daylight; .sky__clouds { type: fluffy; float: center; } .sky__sun { strength: .7; align: center; float: top; } } }
Если вы хотите досконально разобраться в работе BEM, то рекомендую прочитать статью, написанную моим другом и коллегой.
BEM хорош тем, что делает компоненты уникальными #reusabilityFtw. При таком подходе некоторые паттерны становились очевиднее по мере внедрения нового соглашения в наши старые таблицы стилей.
Но при этом возникли и новые проблемы:
- Процедура выбора className превратилась в кропотливую задачу.
- Со всеми этими длинными именами классов разметка стала раздутой.
- Необходимо явно расширять каждый компонент интерфейса при каждом повторном использовании.
- Разметка стала излишне семантической.
CSS-модули и локальное пространство видимости
Некоторые проблемы не смогли решить ни SASS, ни BEM. Например, в логике языка отсутствует концепция истинной инкапсуляции. Следовательно, задача выбора имён классов возлагается на разработчика. Чувствовалось, что проблему можно было решить с помощью инструментов, а не соглашений.
Именно это и сделали CSS-модули: в их основе лежит создание динамических имён классов для каждого локально заданного стиля. Это позволило избавиться от визуальных регрессий, возникавших из-за внедрения новых CSS-свойств, теперь все стили инкапсулировались корректно.
CSS-модули быстро стали популярны в экосистеме React, и сегодня они используются во многих проектах. У них есть свои преимущества и недостатки, но в целом это хорошая, полезная парадигма.
Однако… Сами по себе модули не решают ключевых проблем CSS, они лишь показывают нам способ локализации определений стилей: умный способ автоматизации BEM, чтобы нам больше не пришлось заниматься выбором имён классов (ну или хотя бы заниматься этим реже).
Но модули не снижают потребности в хорошей и предсказуемой архитектуре стилей, простой в расширении и многократном использовании, требующей наименьшего количества усилий для своего управления.
Вот как выглядит локальный CSS:
@import '~tools/theme'; :local(.root) { border: 1px solid; font-family: inherit; font-size: 12px; color: inherit; background: none; cursor: pointer; display: inline-block; text-transform: uppercase; letter-spacing: 0; font-weight: 700; outline: none; position: relative; transition: all 0.3s; text-transform: uppercase; padding: 10px 20px; margin: 0; border-radius: 3px; text-align: center; } @mixin button($bg-color, $font-color) { background: $bg-color; color: $font-color; border-color: $font-color; &:focus { border-color: $font-color; background: $bg-color; color: $font-color; } &:hover { color: $font-color; background: lighten($bg-color, 20%); } &:active { background: lighten($bg-color, 30%); top: 2px; } } :local(.primary) { @include button($color-primary, $color-white) } :local(.secondary) { @include button($color-white, $color-primary) }
Это просто CSS, а его главное отличие в том, что все
className с добавлением :local будут генерировать уникальные имена классов наподобие:.app–components–button–__root — 3vvFf {}
Можно сконфигурировать генерируемый идентификатор с помощью параметра запроса
localIdentName. Пример: css–loader?localIdentName=[path][name]–––[local]–––[hash:base64:5] для облегчения отладки.В основе локальных CSS-модулей лежит простая идея. Они являются способом автоматизации BEM-нотации за счёт генерирования уникального
className, которое не станет конфликтовать ни с одним другим, даже если будет использоваться одно и то же имя. Весьма удобно.Полное вливание CSS в JavaScript с помощью styled-components
Styled-components — это визуальные примитивы, работающие как обёртки. Они могут быть привязаны к конкретным HTML-тегам, которые всего лишь обёртывают дочерние компоненты с помощью styled-components.
Этот код поможет понять идею:
import React from "react" import styled from "styled-components" // Simple form component const Input = styled.input` background: green ` const FormWrapper = () => <Input placeholder="hola" /> // What this compiles to: <input placeholder="hola" class="dxLjPX">Send</input>
Всё очень просто: styled-components использует для описания CSS-свойств шаблонное буквенное обозначение (template literal notation). Похоже, что команда разработчиков попала в точку, объединив возможности ES6 и CSS.
Styled-components предоставляет очень простой паттерн для многократного использования и полностью отделяет интерфейс от компонентов функциональности и структуры. Создаётся API, имеющий доступ к нативным тегам — либо в браузере как HTML, либо нативно используется React Native.
Вот как передаются в styled-components кастомные свойства (или модификаторы):
import styled from "styled-components" const Sky = styled.section` ${props => props.dusk && 'background-color: dusk' } ${props => props.day && 'background-color: white' } ${props => props.night && 'background-color: black' } `; // You can use it like so: <Sky dusk /> <Sky day /> <Sky night />
Как видите, свойства неожиданно стали модификаторами, получаемыми каждым компонентом, и они могут быть обработаны, получая на выходе разные строки CSS. Это позволяет использовать все возможности JS для обработки наших стилей, которые при этом остаются согласующимися и готовыми к многократному использованию.
Основной интерфейс может многократно использоваться кем угодно
Стало быстро понятно, что ни CSS-модули, ни styled-components сами по себе не были идеальным решением. Необходим некий паттерн, чтобы всё это эффективно работало и масштабировалось. Такой паттерн возник из определения, чем является компонент, и его полного отделения от логики. Это позволило создать основные компоненты (core components), единственное предназначение которых — стили.
Пример реализации таких компонентов с помощью CSS-модулей:
import React from "react"; import classNames from "classnames"; import styles from "./styles"; const Button = (props) => { const { className, children, theme, tag, ...rest } = props; const CustomTag = `${tag}`; return ( <CustomTag { ...rest } className={ classNames(styles.root, theme, className) }> { children } </CustomTag> ); }; Button.theme = { secondary: styles.secondary, primary: styles.primary }; Button.defaultProps = { theme: Button.theme.primary, tag: "button" }; Button.displayName = Button.name; Button.propTypes = { theme: React.PropTypes.string, tag: React.PropTypes.string, className: React.PropTypes.string, children: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.element, React.PropTypes.arrayOf(React.PropTypes.element) ]) }; export default Button;
Здесь компонент получает свойства, которые привязаны к дочернему компоненту. Иными словами, компонент-обёртка передаёт все свойства дочернему компоненту.
Теперь ваш компонент можно применить так:
import React from "react" import Button from "components/core/button" const = Component = () => <Button theme={ Button.theme.secondary }>Some Button</Button> export default Component
Продемонстрирую аналогичный пример полной реализации кнопки с помощью styled-components:
import styled from "styled-components"; import { theme } from "ui"; const { color, font, radius, transition } = theme; export const Button = styled.button` background-color: ${color.ghost}; border: none; appearance: none; user-select: none; border-radius: ${radius}; color: ${color.base} cursor: pointer; display: inline-block; font-family: inherit; font-size: ${font.base}; font-weight: bold; outline: none; position: relative; text-align: center; text-transform: uppercase; transition: transorm ${transition}, opacity ${transition}; white-space: nowrap; width: ${props => props.width ? props.width : "auto"}; &:hover, &:focus { outline: none; } &:hover { color: ${color.silver}; opacity: 0.8; border-bottom: 3px solid rgba(0,0,0,0.2); } &:active { border-bottom: 1px solid rgba(0,0,0,0.2); transform: translateY(2px); opacity: 0.95; } ${props => props.disabled && ` background-color: ${color.ghost}; opacity: ${0.4}; pointer-events: none; cursor: not-allowed; `} ${props => props.primary && ` background-color: ${color.primary}; color: ${color.white}; border-color: ${color.primary}; &:hover, &:active { background-color: ${color.primary}; color: ${color.white}; } `} ${props => props.secondary && ` background-color: ${color.secondary}; color: ${color.white}; border-color: ${color.secondary}; &:hover, &:active { background-color: ${color.secondary}; color: ${color.white}; } `} `;
Любопытный момент: компонент получается совершенно тупым и служит только обёрткой CSS-свойств, привязанных к родительскому компоненту. У такого подхода есть преимущество:
Это позволяет нам описывать API базового интерфейса, который можно менять по своему желанию, и при этом все интерфейсы в рамках приложения останутся согласующимися.
Таким образом, мы можем полностью изолировать создание дизайна от реализации. Если нужно, они будут протекать одновременно: один разработчик занимается реализацией фичи, а другой полирует интерфейс, и всё это с полным разделением ответственности.
Звучит превосходно. Казалось бы, нужно следовать этому паттерну. Вместе с ним мы начали искать и другие полезные решения.
Получатели свойств
Эти функции прослушивают свойства, передаваемые какому-либо компоненту. Прямо-таки священный Грааль многократного использования и расширения возможностей любого компонента. Можете рассматривать это как способ наследования модификаторов. Вот что я имею в виду:
// Prop passing Shorthands for Styled-components export const borderProps = props => css` ${props.borderBottom && `border-bottom: ${props.borderWidth || "1px"} solid ${color.border}`}; ${props.borderTop && `border-top: ${props.borderWidth || "1px"} solid ${color.border}`}; ${props.borderLeft && `border-left: ${props.borderWidth || "1px"} solid ${color.border}`}; ${props.borderRight && `border-right: ${props.borderWidth || "1px"} solid ${color.border}`}; `; export const marginProps = props => css` ${props.marginBottom && `margin-bottom: ${typeof (props.marginBottom) === "string" ? props.marginBottom : "1em"}`}; ${props.marginTop && `margin-top: ${typeof (props.marginTop) === "string" ? props.marginTop : "1em"}`}; ${props.marginLeft && `margin-left: ${typeof (props.marginLeft) === "string" ? props.marginLeft : "1em"}`}; ${props.marginRight && `margin-right: ${typeof (props.marginRight) === "string" ? props.marginRight : "1em"}`}; ${props.margin && `margin: ${typeof (props.margin) === "string" ? props.margin : "1em"}`}; ${props.marginVertical && ` margin-top: ${typeof (props.marginVertical) === "string" ? props.marginVertical : "1em"} margin-bottom: ${typeof (props.marginVertical) === "string" ? props.marginVertical : "1em"} `}; ${props.marginHorizontal && ` margin-left: ${typeof (props.marginHorizontal) === "string" ? props.marginHorizontal : "1em"} margin-right: ${typeof (props.marginHorizontal) === "string" ? props.marginHorizontal : "1em"} `}; `; // An example of how you can use it with your components const SomeDiv = styled.div` ${borderProps} ${marginProps} ` // This lets you pass all borderProps to the component like so: <SomeDiv borderTop borderBottom borderLeft borderRight marginVertical>
Пример использования получателей свойств
Это позвол��ет не хардкодить границы для каждого конкретного компонента, что экономит нам кучу времени.
Placeholder / Функциональность наподобие миксина
В styled-components можно использовать весь потенциал JS, чтобы функции были не просто получателями свойств и чтобы разные компоненты могли совместно использовать код:
// Mixin like functionality const textInput = props => ` color: ${props.error ? color.white : color.base}; background-color: ${props.error ? color.alert : color.white}; `; export const Input = styled.input` ${textInput} `; export const Textarea = styled.textarea` ${textInput}; height: ${props => props.height ? props.height : '130px'} resize: none; overflow: auto; `;
Компоненты макета
Мы обнаружили, что при работе над приложением нам в первую очередь нужен макет размещения элементов интерфейса. Поэтому мы определили компоненты, помогающие нам в решении этой задачи. Они очень полезны, поскольку некоторые разработчики (недостаточно знакомые с методиками CSS-позиционирования) часто тратят много времени на создание структуры. Вот пример подобных компонентов:
import styled from "styled-components"; import { theme, borderProps, sizeProps, backgroundColorProps, marginProps } from "ui"; const { color, font, topbar, gutter } = theme; export const Panel = styled.article` ${marginProps} padding: 1em; background: white; color: ${color.black}; font-size: ${font.base}; font-weight: 300; ${props => !props.noborder && `border: 1px solid ${color.border}`}; width: ${props => props.width ? props.width : "100%"}; ${props => borderProps(props)} transition: transform 300ms ease-in-out, box-shadow 300ms ease-in-out, margin 300ms ease-in-out; box-shadow: 0 3px 3px rgba(0,0,0,0.1); ${props => props.dark && ` color: ${color.white}; background-color: ${color.black}; `} &:hover { transform: translateY(-5px); box-shadow: 0 6px 3px rgba(0,0,0,0.1); } `; export const ScrollView = styled.section` overflow: hidden; font-family: ${font.family}; -webkit-overflow-scrolling: touch; overflow-y: auto; ${props => props.horizontal && ` white-space: nowrap; overflow-x: auto; overflow-y: hidden; ` } ${props => sizeProps(props)} `; export const MainContent = styled(ScrollView)` position: absolute; top: ${props => props.topbar ? topbar.height : 0}; right: 0; left: 0; bottom: 0; font-size: ${font.base}; padding: ${gutter} 3em; ${props => props.bg && ` background-color: ${props.bg}; `} `; export const Slide = styled.section` ${backgroundColorProps} font-weight: 400; flex: 1; height: ${props => props.height ? props.height : "100%"}; width: ${props => props.width ? props.width : "100%"}; justify-content: center; flex-direction: column; align-items: center; text-align: center; display: flex; font-size: 3em; color: ${color.white}; `; export const App = styled.div` *, & { box-sizing: border-box; } `;
Компонент
<ScrollView /> получает в виде свойств ширину и высоту, а также свойство горизонтали для появляющейся внизу полосы прокрутки.Вспомогательные компоненты
Они облегчают нам жизнь и позволяют активно заниматься многократным использованием. Здесь мы храним все часто используемые паттерны. Вот некоторые из полезных для меня вспомогательных компонентов:
import styled, { css } from "styled-components"; import { borderProps, marginProps, backgroundColorProps, paddingProps, alignmentProps, positioningProps, sizeProps, spacingProps, theme } from "ui"; const { screenSizes } = theme; export const overlay = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); `; // You can use this like ${media.phone`width: 100%`} export const media = Object.keys(screenSizes).reduce((accumulator, label) => { const acc = accumulator; acc[label] = (...args) => css` @media (max-width: ${screenSizes[label]}em) { ${css(...args)} } `; return acc; }, {}); // Spacing export const Padder = styled.section` padding: ${props => props.amount ? props.amount : "2em"}; `; export const Spacer = styled.div` ${spacingProps} `; // Alignment export const Center = styled.div` ${borderProps} ${marginProps} ${backgroundColorProps} ${paddingProps} ${alignmentProps} ${positioningProps} ${sizeProps} text-align: center; margin: 0 auto; `; // Positioning export const Relative = styled.div` ${props => borderProps(props)}; position: relative; `; export const Absolute = styled.div` ${props => marginProps(props)}; ${props => alignmentProps(props)}; ${props => borderProps(props)}; position: absolute; ${props => props.right && `right: ${props.padded ? "1em" : "0"}; `} ${props => props.left && `left: ${props.padded ? "1em" : "0"}`}; ${props => props.top && `top: ${props.padded ? "1em" : "0"}`}; ${props => props.bottom && `bottom: ${props.padded ? "1em" : "0"}`}; `; // Patterns export const Collapsable = styled.section` opacity: 1; display: flex; flex-direction: column; ${props => props.animate && ` transition: transform 300ms linear, opacity 300ms ease-in, width 200ms ease-in, max-height 200ms ease-in 200ms; max-height: 9999px; transform: scale(1); transform-origin: 100% 100%; ${props.collapsed && ` transform: scale(0); transition: transform 300ms ease-out, opacity 300ms ease-out, width 300ms ease-out 600ms; `} `} ${props => props.collapsed && ` opacity: 0; max-height: 0; `} `; export const Ellipsis = styled.div` max-width: ${props => props.maxWidth ? props.maxWidth : "100%"}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; export const Circle = styled.span` ${backgroundColorProps} display: inline-block; border-radius: 50%; padding: ${props => props.padding || '10px'}; `; export const Hidden = styled.div` display: none; `;
Тема
Тема — это единый источник правдивых значений, которые можно многократно использовать по всему приложению. В ней полезно хранить такие вещи, как палитра цветов или общий стиль.
export const theme = { color: { primary: "#47C51D", secondary: '#53C1DE', white: "#FFF", black: "#222", border: "rgba(0,0,0,0.1)", base: "rgba(0,0,0,0.4)", alert: '#FF4258', success: 'mediumseagreen', info: '#4C98E6', link: '#41bbe1' }, icon: { color: "gray", size: "15px" }, font: { family: ` -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'`, base: '13px', small: '11px', xsmall: '9px', large: '20px', xlarge: '30px', xxlarge: '50px', }, headings: { family: 'Helvetica Neue', }, gutter: '2em', transition: '300ms ease-in-out' }; export default theme;
Преимущества
- Вся мощь JS у нас в руках, полное взаимодействие с интерфейсом компонента.
- Не нужно с помощью
classNameсвязывать компоненты и стили (это делается без вашего участия). - Огромное удобство разработки, не приходится забивать себе голову именами классов и их привязкой к компонентам.
Недостатки
- Ещё нужно тестировать на реальных проектах.
- Создано для React.
- Проект очень молодой.
- Тестирование надо проводить через
aria-labelили с помощьюclassName.
Заключение
Какую бы технологию вы ни использовали — SASS, BEM, CSS-модули или styled-components, — не существует заменителя для хорошо продуманной архитектуры стилей, позволяющей разработчикам интуитивно развивать кодовую базу, без долгих и мучительных обдумываний, без ломания или внедрения новых подвижных частей системы.
Такой подход необходим для корректного масштабирования, и его можно достичь даже при условии использования чистого CSS и BEM. Всё дело лишь в объёме работы и LOC, необходимых для каждой реализации. В целом styled-components можно назвать подходящим решением для большинства React-проектов. Его ещё нужно активно тестировать, но проект выглядит многообещающе.
