Jeff Walker Code Ranger
Jeff Walker Code Ranger

Reputation: 4926

CSS Best Practices for decoupled modules

The answer to this question is not pointing out some css selector or property that I am unaware of. It is also not throwing together some random css that makes this specific case work. Off the top of my head I can think of several ways to make this specific example work. I'm sure there are hundreds more.

What are the best practices for creating CSS such that various design elements are decoupled?

Explanation of what I mean:

I am a computer programmer with an ok design sense. When writing good code I look to create classes/objects that are decoupled, meaning there are not strange and unexpected interactions between them. I can mix and match my classes/objects freely and the results work well and are what you would expect. All my attempts to learn/create CSS best practises don't work out this well. I've been a .NET web developer for 10+ years now. For a long time I believed in semantic CSS. I loved csszengarden.com. I've tried to learn OOCSS and SMACSS. Despite all that I can't get my CSS to work the way I can get my code to work. I search online for CSS best practices and find things like naming, formatting and a few tips and tricks. Never a deep understanding of how to create decoupled CSS. Perhaps it is just impossible. I don't feel like it should be. I feel like it should be possible to create a design language of reusable elements that can be composed.

Since all that is very abstract and it is hard to discuss without an example. Here is an example of the challenges I run into. This is based on a situation involving bootstrap, but I have simplified the styles. So please understand the styles are the way they are because that is what makes sense for the rest of the site and this example isn't about some trivial change that makes it work in this exact case.

Example:

Code for this is on jsbin.

I have a panel module with header and content. Typically the header contains an h2 and one or more button actions:

Basic Module

Note, the equal padding around the header and the actions float right. This design is responsive and when the panel is narrow, the actions must drop below the title. Though there is actually an issue when that happens in that there is no space between the title and button.

But really, the panel is a module that can have anything it its header. This should follow the OOCSS principle of "separation of containers and contents". So it shouldn't really matter what we put in the panel header. We want it to work well.

Now on a particular page it make sense to put a select list in the panel header. Like with Bootstrap there are many styles that pertain to forms, so we use these styles here as well. The result looks something like:

Module with Form in Header

Notice that because the form-group (per Bootstrap) has a bottom margin there is now double the space at the bottom of the header (the bottom margin provides correct spacing in forms with multiple form groups). I am in agreement with our designer that the double space is wrong, it should be an equal amount of space as the top (like in the simpler example). I found a good article on ways to try to deal with this. However, the "best" option at the end (uses *:last-child) which is the one I like doesn't work here because the form is not the last element in the container because the action button must float below the select list when the window is small. Also, I feel like situations like that can always arise. Note that in this case when the window is small and the button floats below the select, the spacing is good because the margin on the form-group provides spacing between them.

Additionally, the designer says the button should be vertically aligned with the select (looks better with bootstrap because the inputs are the same height). It feels like there is no way to accomplish that which isn't very specific to the particular combination of elements here or to the particular page this appears on. That is, I can't imagine a generic best practice for making things like that line up right.

CSS for the above is too long to include in this already long question, but again check out the jsbin.

Restatement of Question:

Again, I am not looking for specific CSS that will fix this particular example. I want to know what best practices will allow me to create CSS for decoupled design elements that can be freely combined without constantly running into issues like the above.

Upvotes: 4

Views: 1048

Answers (2)

gfullam
gfullam

Reputation: 12035

On composability in CSS

Position, dimensions of children should be responsibility of parents

What are the best practices for creating CSS such that various design elements are decoupled?

I have given this subject considerable thought and I was glad to have been sent a link to your article "CSS is Not Composable" by a colleague, which is how I found this question.

Because CSS intrinsically lends itself to creating selectors in a global space, separating decoupled modules into namespaces is a wise best practice.

You may optionally define base styles for a generic component class at the global level, but rely on namespace classes and/or IDs to apply positional and dimensional styles to the components based on where they are included.

Do not apply positional or dimensional styles directly to any component container. All other styles are defined by the component within its namespace.

In other words, the position and dimensions of a child component should be the responsibility of the parent container.

Correct:

/* A component namespace */
.my-component-A {
    padding: 10px;
    background-color: White;
}

/* A component styles its children as necessary */
.my-component-A > p {
    margin: 0 0 1em 0;
}

/* Position & dimension applied by parent (Page A) within its namespace */
.my-page-A .my-component-A { 
    float: left;
    margin: 0 10px 0 0;
}

/* Position & dimension applied by parent (Page B) within its namespace */
.my-page-B .my-component-A { 
    float: right;
    margin: 0 0 0 10px;
}

/* Position & dimension applied by parent (Component B) within its namespace */
.my-component-B .my-component-A {
    margin: 10px;
}

Incorrect:

/* Position & dimension applied by component within its own namespace */
.my-component-A {
    float: left;
    margin: 0 10px 0 0;
    padding: 10px;
    background-color: White;
}

/* Position & dimension now have to be overridden by parent */
.my-component-B .my-component-A {
    float: none;
    margin: 10px;
}

By using the parent component or page namespace to apply position and dimension to child components, you create a system of fully composable decoupled modules. And because a component will never change its own size or position, the developer who maintains the parent component or page has assurance that they are in control of the layout of its children.

If you are consistent and intentional, each component is capable of being placed into any other component. And because it doesn't define its own size or position, it flows into the parent container with its browser default positional and dimensional styles or with the styles provided by your global reset.

You, as the developer who maintains a page or parent component, have complete freedom to modify the layout of everything under that namespace without needing to apply overrides. And if your team is on board with this best practice, you don't need to worry that namespaced style changes made to a child component maintained by someone else will break the layout of the page or parent component you maintain.

Every component is position- and dimension-ignorant, relying entirely on its parent component or page to define its placement and size as necessary.

In your example, we can apply this principal to create a form group component that can be placed on a page and into other components at various depths while relinquishing control over layout to the page or parent component.

/* Component Base / generic reset */
.component {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Use generic page and component class to target all child components at once */
.pagewrapper > .component {
  margin: 20px;
}

/* Component: Panel */
.component.panel {
  /* No positional styles defined on component container */
  border: 1px solid Black;
}
.component.panel .panel-header {
  /* `display: table` is a great legacy & cross-browser solution for vertical alignment */
  display: table;
  position: relative;
  overflow: auto;
  border-bottom: 1px solid Gray;
}
.component.panel .panel-header > * {
  /* set all immediate children as table-cells to get vertical-alignment */
  display: table-cell;
  padding: 10px;
  table-layout: auto;
  border-collapse: collapse;
  vertical-align: middle;
}
.component.panel .panel-header > .actions {
  width: 1%;
}
.component.panel .panel-header h2 {
  margin: 0;
  font-size: 25px;
}
.component.panel .panel-content {
  /* Exclude bottom padding and apply margin-bottom to immediate children instead */
  padding: 10px 10px 0;
}
.component.panel .panel-content > * {
  /* All immediate children of the container get a bottom margin */
  margin: 0 0 10px;
}

/* Component: Form Group */
.component.form-group {
  /* No positional styles defined on component container */
  padding: 10px;
  background-color: Beige;
}
.component.form-group label {
  display: block;
}

/* Component position and dimension are namespaced to the parent */
.pagewrapper.home > .component.form-group {
  /* Use child selector to limit scope of namespaced components where a component may exist multiple times as descendants of the same ancestor. */
  /* You may override any other styles such as background-colors */
  background-color: Lavender;
}
.component.panel .panel-header .component.form-group {
  /* Positional style is always applied in the context of a parent container… */
  margin: -10px;
}
.component.panel .panel-content .component.form-group {
  /* …because it may need different positioning even within the same parent component. */
  margin: 0 -10px;
}
.component.panel .panel-content .component.form-group:first-child {
  /* Strategically use pseudo-class selectors… */
  margin: -10px -10px 0;
}
.component.panel .panel-content .component.form-group + * {
  /* …and sibling selectors (and other selectors as warranted). */
  margin-top: 10px;
}
<main class="pagewrapper home" id="my-page-A">

  <!-- Position and dimension defined by page…
       `.pagewrapper > .component` -->
  <section class="component panel" id="my-panel-A">
    <div class="panel-header">
      <h2>Title</h2>
      <div class="actions">
        <button>Add</button>
      </div>
    </div>
    <div class="panel-content">
      <p>Content</p>
    </div>
  </section>

  <section class="component panel" id="my-panel-B">
    <div class="panel-header">

      <!-- Position and dimension defined by parent component…
           `.component.panel .panel-header .component.form-group` -->
      <div class="component form-group" id="my-form-group-A">
        <label>A Label</label>
        <select>
          <option>Something</option>
        </select>
      </div>
      
      <div class="actions">
        <button>Add</button>
      </div>
    </div>
    <div class="panel-content">
      <p>Content</p>
      <p>More content</p>
    </div>
  </section>

  <section class="component panel" id="my-panel-C">
    <div class="panel-header">
      <h2>Title</h2>
      <div class="actions">
        <button>Add</button>
      </div>
    </div>
    <div class="panel-content">
      <p>Content</p>
      
      <!-- Position and dimension defined by parent component…
           `.component.panel .panel-content .component.form-group` -->
      <div class="component form-group" id="my-form-group-B">
        <label>A Label</label>
        <select>
          <option>Something</option>
        </select>
      </div>

      <p>More content</p>
    </div>
  </section>

  <!-- Position and dimension defined by namespaced page…
       `.pagewrapper.home > .component.form-group` -->
  <div class="component form-group" id="my-form-group-C">
    <label>A Label</label>
    <select>
      <option>Something</option>
    </select>
  </div>

</main>

Upvotes: 3

IMI
IMI

Reputation: 2469

CSS is not meant to be separate. It is intended to have a parent to child cascading effect and can be quite powerful, reliable and resilient when applied well. OOCSS is close to what you want but not quite. With OOCSS the intention is to put common attributes in a single class that can be reused for many instances.

I think what you may want to do is look into Web Components and the use of Shadow DOM. It allows you to create your widgets so that they have their own styling separate from the main page DOM and can reduce the chance for unwanted styling even further.

EDITED: I know you said you are looking for best practices and not solutions. However, I felt I should provide some possible code samples as possible "best practice" solutions to your posted situation. For more control you you should utilize more specific css selectors as they will be given the higher importance when rendering. As a "best practice" for greatest control you should use containers with unique ID's that you can use to target sections of your html with greater specificity (ie. #mainContent, #sideBar, #myWidgetName, etc.). Then you can pair these with various CSS3 Selectors to increase the specificity and achieve greater control.

TLDR: General CSS Best Practices

  1. Start with Base styles (OOCSS is good for this)
  2. Add unique ID's to your important container elements
  3. Use Container ID's, classes, CSS3 selectors, etc. to specifically target elements inside those containers and override base styles as needed
  4. Also keep in mind that where you place your rules are important (top of stylesheet, bottom of stylesheet, inline, etc.)

/* ----- YOUR ORIGINAL STYLES -----*/

/* General form styles */
.form-group {/* like bootstrap, need space between form inputs */margin-bottom: 10px;}
.form-group label {display: block;}

/* Panel styles */
.panel {border: 1px solid black; background-color: white; margin: 20px;}
.panel-header {padding: 10px; border-bottom: 1px solid grey;}
.panel-header h2 {font-size: 25px; margin: 0px; display: block; float: left;}
.panel-header .form-group {float: left;}
.panel-header .actions {float: right; margin-top: 2px; /* nudge down to try and line up with title */}
.panel-content {padding: 10px;}

/* generic clear fix */
.clearfix:after {content: ""; display: table; clear: both;}




/* ----- SUGGESTED SOLUTION STYLES -----*/
.panel-header {position:relative;/* will assist with button alignment */}
.panel-header > *:nth-child(n) {
    margin-bottom: 0;/* zero out the bottom margin of all direct children of ".panel-header" */
}
@media (min-width: 768px) {
.panel-header .actions { /* now that the bottom margins are set this will position the button to be aligned to the bottom of the container */
    bottom: 10px;/*matches .panel-header margin*/
    float: none;
    position: absolute;
    right: 10px;/*matches .panel-header margin*/
}
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div class="panel">
    <div class="panel-header clearfix">
      <h2>Title</h2>
      <div class="actions">
        <button>Add</button>
      </div>
    </div>
    <div class="panel-content">
      Content
    </div>
  </div>
  
  <div class="panel">
    <div class="panel-header clearfix">
      <div class="form-group">
        <label>A Label</label>
        <select>
          <option>Something</option>
        </select>
      </div>
      <div class="actions">
        <button>Add</button>
      </div>
    </div>
    <div class="panel-content">
      Content
    </div>
  </div>
</body>
</html>

Upvotes: 0

Related Questions