С самого начала истории интернета мы нуждались в стилях для наших сайтов. Многие годы нам для этого служил 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-проектов. Его ещё нужно активно тестировать, но проект выглядит многообещающе.
Автор: Mail.Ru Group