Skip to content
← All posts
6 min read Dawid Skłodowski

HAML and SASS: markup and styles that stay DRY

Two tools we reach for on every Rails project. How HAML makes templates readable and SASS keeps stylesheets maintainable — with the patterns that work and the traps (over-nesting, @extend) that don't.

Two tools sit on the front-end of nearly every Rails project we build: HAML for templates and SASS for stylesheets. Neither is essential — ERB and plain CSS work — but both pay for themselves within a week by removing a category of noise and repetition that otherwise accretes until your views and stylesheets become the part of the codebase nobody wants to touch. Here is how we use them, and the mistakes we have learned to avoid.

HAML: the markup disappears

ERB is HTML with Ruby sprinkled in, which means you write — and read — every angle bracket and every closing tag. HAML throws all of that away. Indentation defines nesting, so there are no closing tags to mismatch; %tag declares an element, and the CSS-style .class and #id shorthands mean the common case reads almost like the selectors you will later style:

.card
  .card__header
    %h2= post.title
    %span.card__meta= post.published_on.to_s(:long)
  .card__body
    = simple_format(post.body)
  - if post.comments.any?
    %footer.card__footer
      = pluralize(post.comments.count, "comment")

The equivalent ERB is a third longer and a good deal noisier, and it is much easier to get wrong — a missing </div> in deeply nested ERB is a classic afternoon-killer that simply cannot happen in HAML, because there are no closing tags. The discipline HAML imposes is real, too: because indentation is the structure, sloppy nesting becomes visible immediately, and your templates end up shallower and better-factored almost by force.

A few things worth knowing once you are past the basics:

  • = outputs an expression, - runs Ruby without outputting. %h2= post.title prints; - if ... controls flow.
  • Attributes are a Ruby hash: %a{ href: post_path(post), class: "btn" }, and the hash plays nicely with Rails helpers and conditional attributes.
  • Filters handle embedded content cleanly — :javascript, :css, :markdown — so you are not escaping your way through inline blocks.
  • HAML rewards extracting partials and helpers, because a template that is all structure makes a stray lump of logic stand out. When a view starts sprouting conditionals, that is HAML telling you to move logic into a helper or a presenter.

The objection people raise is whitespace-sensitivity, and it is fair: a botched indent is a real error. But it is an error you see at once, in the place it happens, rather than a malformed-DOM mystery you debug in the browser. We will take that trade every time.

SASS: stylesheets that scale

CSS does not let you abstract. You cannot name a colour, share a block of declarations, or nest related rules — so a growing stylesheet becomes a long flat list with the same hex code copied forty times and selectors repeated for every variant. SASS (we use the SCSS syntax, which is a superset of CSS, so any valid CSS is valid SCSS) gives you the missing tools: variables, nesting, mixins, inheritance, and partials.

Variables and partials: a single source of truth

// _variables.scss
$brand:        #b00020;
$ink:          #1a1a1a;
$space:        1rem;
$breakpoint-m: 48rem;

// application.scss
@import "variables";
@import "components/card";
@import "components/buttons";

Naming your palette and spacing once, in a partial everything imports, is the single highest-leverage thing SASS gives you. Rebrand from one file; change your spacing rhythm in one place. The @import partials (filenames prefixed with _) also let you split a monolithic stylesheet into a file per component, which is how you keep it navigable as it grows.

Mixins for reuse, with arguments

A mixin is a reusable block of declarations, optionally parameterised — ideal for the vendor-prefix soup of 2014 and for responsive breakpoints:

@mixin respond-to($min) {
  @media (min-width: $min) { @content; }
}

.card {
  padding: $space;
  @include respond-to($breakpoint-m) {
    padding: $space * 2;
  }
}

The @content block is what makes responsive mixins click: you name your breakpoints once and write media queries that read like English everywhere else.

Nesting: useful, until it isn’t

Nesting is the feature people reach for first and abuse hardest. It mirrors your markup structure and keeps related rules together:

.card {
  &__header { font-weight: 600; }
  &__body   { color: $ink; }
  &:hover   { box-shadow: 0 2px 8px rgba(0,0,0,.1); }
}

The & (parent selector) is the good part — combined with a naming convention like BEM (block__element--modifier), it produces flat, low-specificity selectors while keeping the source neatly grouped. The trap is nesting that follows the DOM tree several levels deep:

// DON'T: this compiles to .sidebar .widget ul li a — fragile and over-specific
.sidebar { .widget { ul { li { a { color: $brand; } } } } }

That generates deeply descendant, highly specific selectors that are brittle (break if the markup moves), hard to override (you end up at war with specificity, reaching for !important), and slow to read. Our rule of thumb: nest for the & modifier pattern, not to mirror the HTML. If a selector is more than two or three levels deep, flatten it, usually by giving the element its own class.

@extend vs mixins: prefer the mixin

SASS offers two ways to share declarations, and the choice matters more than it looks. @extend makes one selector inherit another’s rules by grouping selectors in the output; a mixin copies declarations in. @extend looks more elegant and produces less CSS, but it has surprising effects: it can reorder your output, yank selectors across the cascade in ways that change which rule wins, and create sprawling comma-lists that are hard to predict. After being bitten by @extend-induced specificity surprises, our default is the mixin (or a placeholder selector %foo with @extend only for genuinely shared, static chunks). The few extra bytes are worth the predictability.

They work together

The reason these two pair so well on a Rails project is that they push in the same direction: structure over noise. HAML’s .card__header element sits directly above SASS’s .card { &__header { ... } } rule, the names line up, and the asset pipeline compiles both without ceremony. A component is a HAML partial plus a SCSS partial that share a name and a BEM root — a unit you can read, move, and reason about as a whole.

Neither tool is magic, and both can be abused — HAML into clever one-liners nobody can read, SASS into nesting pyramids and @extend spaghetti. Used with a little discipline, though, they remove the busywork from the front end and leave you with templates and stylesheets that are still pleasant to open a year later. On our projects that is the whole game: the code you are not afraid to change is the code that keeps shipping.