Design Systems, Pt. 7: Project Structure
The previous articles explained the building blocks in detail, but not how to put everything together. Let’s see how all of this would look in a project.
Folder layout
├── _extends.scss
├── _functions.scss
├── _mixins.scss
├── main.scss
├── base
│ ├── _general.scss
│ ├── _index.scss
│ └── _typography.scss
├── components
│ ├── _index.scss
│ └── _pagination.scss
├── export
│ └── _colors.scss
├── layouts
│ ├── _index.scss
│ └── _main-grid.scss
├── themes
│ ├── _base.scss
│ ├── _dark.scss
│ ├── _default.scss
│ ├── _functions.scss
│ └── _index.scss
└── utilities
├── _colors.scss
├── _index.scss
└── _spacing.scss
Root
_extends.scss
: Contains all placeholder selectors._functions.scss
: Contains all functions, except those for theming._mixins.scss
: Contains all mixins, including the one for defining the custom properties.main.scss
: The entry point.
main.scss
only uses the other modules of the system.
@charset 'utf-8';
@use "base";
@use "components";
@use "layouts";
@use "themes";
@use "utilities";
In the folder tree above you’ll notice the _index.scss
files in the sub
folders. When you @use
a folder, SASS looks for the _index.scss
file in that
folder. The index files in turn should @use
the modules in the same folder.
@use
is preferred over @import
to restrict the scope of variable, function,
and mixin definitions. Unlike @import
, variables are not pulled into the
global scope when you use @use
.
The underscore in file names is a SASS naming convention for partials.
Base
This folder contains global styles. You’ll commonly find these files in that folder:
_general.scss
_animations.scss
_typography.scss
_reset.scss
or_normalize.scss
Components
Components are the building blocks of the page. This could be a navigation bar, a pagination component, a button etc. Each component gets its own file. Components are about the content, not about the relationship to other components or elements.
Layouts
Layouts describe the relationship between components/elements on a page, but do not describe the styles of the contents. You can think of these as wrappers with slots that can be filled with any arbitrary content. Layouts can also be made up of other smaller layouts. Each layout gets its own file. A layout can be anything between a container that aligns two items next to each other and a full-size grid layout with navbar, sidebar and footer.
Export
This folder contains the export modules for interoperation with JavaScript as outlined in part 6 of the series.
Themes
The theme definitions are explained in detail in part 5 of the series.
Utilities
A collection of utility classes, i.e. classes that do one thing and one thing only. This includes manually defined utility classes (e.g. visibility helpers) and classes generated automatically from the theme configuration, as described in part part 4.
Hints for writing clean CSS
Components and layouts
- Each component and each layout should get its own file.
- All styles of a component or layout should be wrapped in a class.
- All sub classes of a component should be prefixed.
- Don’t overdo the nesting.
- Use the child selector where possible.
.pagination {
& > .pagination-link {
// ...
}
& > .pagination-prev {
// ...
}
& > .pagination-next {
// ...
}
}
I prefer not to use a naming convention for components (blocks) like BEM. Just use snake case, and keep the component classes short, since otherwise you’ll get very long names for your sub classes.
Modifier classes
Commonly used naming conventions for modifiers are:
--active
in BEM-active
in RSCSSis-active
in frameworks like Bulma
All of them are readable. BEM appends modifier class names to the block and element classes, resulting in long class names. We use the double dash for custom properties now, and maybe it is better to use it in one context only. The combination of a period and a single dash as in RSCSS can look weird if a font with ligatures is used.
is-*
is sometimes combined with has-*
and are-*
. is-*
and has-*
are
good, human-readable names. They are still snake case, so you don’t need
exceptions in your linter rules. I would avoid are-*
(which is sometimes used
if the class name is a plural), since it is easy to mix up the class names, and
even if the class name is a plural, it is still a single class.
.pagination {
& > .pagination-link {
&.is-current {
// ...
}
}
}
.container {
&.has-sidebar {
// ...
}
}
Using functions, mixins and extends
All theme variables are accessed via the accessor
functions.
To use them, you will need to @use
the module. The same applies to the mixins
and extends.
Functions and mixins from another module can only be used with the module name. Placeholder selectors (extends) can be accessed without the module name.
It is recommended to use aliases when using the root modules.
@use "../extends";
@use "../functions" as f;
@use "../mixins" as m;
.some-component {
@extend %some-placeholder;
@include m.some-mixin($color: f.color(primary));
margin-bottom: f.lines(1);
}
Variables in components and variables
Since variables are locally scoped with @use
, there is no need to prefix any
variables that are specific to a component or layout. You can just put them at
the top of the module.
$max-width: 80%;
.container {
max-width: $max-width;
}
However, you should only use SASS variables if you need to make calculations at compile time, generate styles, or if you need to access a value from another module. In all other cases, it is preferred to use custom properties.
.container {
--max-width: 80%;
max-width: var(--max-width);
}
You can use custom properties for modifying styles.
.container {
max-width: var(--max-width, 80%);
&.is-narrow {
--max-width: 50%;
}
}
Summary
Whatever you do, be consistent. Now keep your classes for layouts (relationships) and components (contents) clean, and that’s it. We fixed CSS.
References
Next part: Design Systems, Pt. 8: Layout Rules