Components are the discrete custom elements of a UI that enclose specific semantics and styling, they also make up the bulk of a UI. Some examples:
- Button
- Pagination
- Breadcrumbs
- Dialog
- Icon
Components are extremely focused implementing only a single part of a UI, so they should never try to do too much. They also shouldn't have any dependencies on ancestral context i.e. where they live in a UI. This makes them extremely portable and robust. Each component should be designed to stand-alone.
Before utilising components consider the layout modules
and objects. Many of the styles you need will exist
in these two sections. In fact your components should hardly ever need any CSS
concerned with layout. Utilities should also be considered if you are needing to implement simple, universal patterns such as
hiding an element visually (.u-hide-visually
), but these are generally used
sparingly.
Take this common UI pattern:
We can see that this fits the criteria of a component because it:
- is a discrete custom element of the UI,
- encloses specific semantics and styling,
- can exist completely stand-alone.
So in this case we would make the call to create a Pagination component.
HTML for the pagination component:
<nav class="c-pagination" role="navigation" aria-label="Pagination">
<ul class="c-pagination__list">
<li class="c-pagination__list__item c-pagination__list__item--count">Pages 1-20 of 200</li><!--
--><li class="c-pagination__list__item">
<a href="#" class="c-pagination__link" rel="prev">← Prev<span class="u-hide-visually">ious page</span></a>
</li><!--
--><li class="c-pagination__list__item">
<a href="#" class="c-pagination__link"><span class="u-hide-visually">Page </span>1</a>
</li><!--
--><li class="c-pagination__list__item">
<a href="#" class="c-pagination__link"><span class="u-hide-visually">Page </span>2</a>
</li><!--
--><li class="c-pagination__list__item">
<a href="#" class="pagination__link is-active"><span class="u-hide-visually">You're currently reading page </span>3</a>
</li><!--
--><li class="c-pagination__list__item pagination__list__item--skip">
<a href="#" class="c-pagination__link"><span class="u-hide-visually">Jump to page </span>21</a>
</li><!--
--><li class="c-pagination__list__item">
<a href="#" class="c-pagination__link"><span class="u-hide-visually">Page </span>22</a>
</li><!--
--><li class="c-pagination__list__item">
<a href="#" class="c-pagination__link"><span class="u-hide-visually">Page </span>23</a>
</li><!--
--><li class="c-pagination__list__item">
<a href="#" class="c-pagination__link" rel="next">Next →<span class="u-hide-visually"> page</span></a>
</li>
</ul>
</nav>
CSS for the pagination component:
/* ============================================================================
@COMPONENTS -> PAGINATION
========================================================================= */
/**
* Pagination component rendered as an inline list.
*/
/**
* Settings.
*/
// Colours
$c-pagination-border-color: darken(#eee, 4%);
.c-pagination {
@include to-rem(padding, $spacing-quarter);
border: 1px solid $c-pagination-border-color;
border-radius: $border-radius;
}
/**
* The list `ul`, render inline.
*/
.c-pagination__list {
@extend %o-list-inline;
@extend %o-list-inline--spacing-tiny;
text-align: center;
}
/**
* List items.
*/
/**
* Modifier: skip list item.
*
* Renders an ellipsis which breaks apart list items.
*/
.c-pagination__list__item--skip:before {
// This right spacing needs to equal the spacing of the list items
@include to-rem(margin-right, $spacing-third);
content: "\2026";
display: inline-block;
vertical-align: bottom;
speak: none;
color: $color-text-base;
}
/**
* The links.
*/
.c-pagination__link {
@include to-rem(padding, $spacing-micro $spacing-third);
display: inline-block;
background-color: #eee;
border: 1px solid $c-pagination-border-color;
border-radius: $border-radius;
/**
* Hover and active states.
*/
&:hover,
&:focus,
&.is-active {
background-color: lighten($color-black, 4%);
color: $color-white;
text-decoration: none;
}
}
As mentioned above your components should avoid having any CSS concerned with layout as that's the job of the layout modules. The most common layout modules you will need are the Grid and the Side-by-side layout modules. No matter how small your layout is, always use a layout module. The layout module classes should be applied directly to the components HTML.
So if we look at this UI:
We can see that we need some layout to render the circle containing the user's initials to the left of the user's information. We might create a component called User account submit button for this but we'd apply the layout module classes direct to the components HTML like so:
<button class="c-button-user-account" type="submit">
<span class="l-side-by-side-alt">
<span class="l-side-by-side-alt__left">
<span class="c-button-user-account__avatar" aria-hidden="true">c</span>
</span>
<span class="l-side-by-side-alt__right">
<strong class="c-button-user-account__name">Nike</strong>
<span class="c-button-user-account__url">nike.createsend.com</span>
<time class="c-button-user-account__last-access" datetime="2009-11-13">Last accessed Today</time>
</span>
</span>
<span class="c-button-user-account__arrow"></span>
</button>
In summary, the rule is; never write any CSS concerned with layout for your components if it can be taken care of by one of the layout modules and apply the layout module classes direct to the components HTML.
Objects should be treated the same as layout modules in that
they should always be used if they can. The difference is that objects should
be @extend
ed within the component partial to avoid having object classes
littered in the components HTML. This allows components to be more easily
updated and keeps a component self-contained.
The pagination component demo'd above is @extend
ing the
List inline object and one of it's
modifiers, like so:
.c-pagination__list {
@extend %o-list-inline;
@extend %o-list-inline--spacing-tiny;
text-align: center;
}
In summary, the rule is; if an object exists that gives you the CSS you need then
use it in your component by @extend
ing it's silent placeholder selector, if
@extend
isn't suitable then use the object's class direct in the components
HTML.
Utilities should be avoided in your components as they typically exist to apply styles to elements that don't already have any style hooks in place.
Ideally utilities should be @extend
ed within the component partial. There are
times when this isn't possible and it is OK to use a utility class directly in
the HTML of the component. This technique should be used sparingly, and only
when a component class doesn't already exist. The most common use case for
this is when you want to hide an element but only visually. If we look at a snippet of the HTML from the pagination component demo'd above we can see this utility: .u-hide-visually
being applied in the HTML:
<li class="c-pagination__list__item">
<a href="#" class="c-pagination__link" rel="next">Next →<span class="u-hide-visually"> page</span></a>
</li>
One exception to using utilities sparingly on components is when you need to apply whitespace (margin
) to the outside of a component, you apply this
whitespace using the Spacing utility, this keeps components extremely Portable and robust and
Free of constraints. So if we wanted to apply
a bottom margin to the pagination component demo'd above we would apply a
spacing utility like so:
<nav class="c-pagination u-s-mb-base" role="navigation" aria-label="Pagination">
In summary, the rule is; never apply single line declaration utilities e.g.
.u-float-right {float: right;}
to a component unless it's the Spacing utility,
the rest of the time only use utilities concerned with simple, universal patterns
(multiline declarations) e.g. hide an element but only visually:
.u-hide-visually
, pin an element to all corners of it's parent:
.u-position-pin-all
, clear a float: .u-clear-fix
, etc. And ideally @extend
utilities via their silent placeholder selectors.
Components should try their best to not be concerned or have any dependencies on ancestral context i.e. where they live in a UI. What this means is that components—if built well—can be moved to different parts of a UI without breaking, making them extremely portable and robust.
To demonstrate this, let's say there is a requirement to also feature the pagination component demo'd above in another part of the UI e.g. a dialog component. The dialog component has a lot less real estate for the pagination component to fit into meaning the component has to be modified in some way to accommodate this, basically we need a compact version.
We could apply these modifications by relying on the components ancestral context i.e. via the components parent element which in this case is the dialog component, so something like this:
CSS
.c-dialog .c-pagination {
[...]
}
HTML
<div class="c-dialog">
<nav class="c-pagination"> [...] </nav>
</div>
However this isn't the most optimal way of handling things as it's highly likely that having a compact version is something we're going to need in other parts of the UI—if not now most likely down the track—therefore it should exist within the pagination component itself not tied to another component.
The correct way to handle this is to use the concept of a BEM modifier like so:
CSS
.c-pagination--compact {
[...]
}
HTML
<nav class="c-pagination c-pagination--compact">
As shown in the example above components can exist within other components i.e. the pagination component can exist in the dialog component.
Nested components like this are perfectly fine. It is important though that components try their best not to be dependent on other components as covered above in the previous section.
However saying that this isn't always black and white (like with many things CSS related) and you will sometimes need to make some slight adjustments to a component when it's nested within another component but this should be scrutinised over to make sure it's the most optimal way of handling things.
The last thing you want is a setup where you have many cross dependencies across your components, as things can fast become a house of cards.
Here is one example of when you might need to make adjustments to a component when it's nested within another component; say you have a Button component which is nested within a Site header component, and by site header I mean the main header of the site template where you'll typically find things like the site logo, main navigation, global search, etc.
So we may have a CTA: Log In in the site header component which uses the button component but in this context the button component needs some slight adjustments. The first thing to consider is creating a BEM modifier for the button so that the modified version is available to be used anywhere in the UI, not just the site header. However if you feel a BEM modifier isn't the right choice because the adjustments are extremely specific to the site header then you will make the adjustments within the site header component partial.
When you do this you include a comment so that other developers are aware of the relationship between the components, like so:
.c-site-header {
[...]
/**
* Extend `.c-button` in Components -> Button.
*/
.c-button {
@include to-rem(padding-left padding-right, 8);
@include font-size($font-size-x-small);
}
Components should be free of widths, margins, and in most cases positioning. This allows components to be extremely portable as they can better adapt to the dimensions of an ancestral context.
Avoiding widths and margins is the most crucial here. If we use the pagination component demo'd above and add these styles to the base class:
.c-pagination {
@include to-rem(width, 400);
@include to-rem(margin-bottom, $spacing-base);
[...]
}
We've now fixed this component to always have a rigid width of 400px
and a
bottom margin of what $spacing-base
is, let's say 24px
. This may be what
you want when you first create the component but adding these default styles is
shortsighted and greatly reduces it's reuse because when you need to reuse the
component in a different part of the UI—or if the UI it will only ever exist
in changes—then the component will most likely break and/or cause breakages to
UI surrounding it.
So all components should be 100% fluid i.e. do not define fixed widths. A components width is always determined by the dimensions of an ancestral context.
When creating whitespace (margin
) outside of a component e.g. a bottom margin
of 24px
, it's best not to bake this into the default component styles as
demonstrated above, and instead apply it via one of the
Spacing utility classes e.g.
<nav class="c-pagination u-s-mb-base">
Components should be encapsulated as much as possible, even if that means your CSS is not as DRY as you think it could be. The main aim is to prevent styles from leaking outside of the component, this isolation prevents avoidable complexity and results in higher code reuse.
An excellent way to test the overrall portability and robustness of your
components is to move the component from where it's intended to live in the UI
to another location in the DOM e.g. directly after the opening body
element.
The component should be the full width of it's parent element, so if that's
the body
element—and the body
/html
elements do not have a fixed width set
on them—then your component should be the width of the viewport.
Or better still create a UI component library where all of your components are dumped into the one web page.
All component classes, settings, and filenames are prefixed with c-
so that
they're easily identifiable e.g.
.c-pagination
$c-pagination-foreground-color
_c-pagination.scss
Make sure to read the documentation within each component Sass partial file as it will contain information about the specific component and it's implementation.
- More Transparent UI Code with Namespaces -> Component Namespaces
- SMACCS
- SUIT CSS components
- CSS guidelines
- Principles of writing consistent, idiomatic CSS
- OOCSS code standards
- SOLID CSS
- One Module or Two
- Our (CSS) Best Practices Are Killing US
- The single responsibility principle applied to CSS
- Principles for writing good CSS
- How We’re Using Modules to Organize Our Front-End Code