AI websites that design themselves

Join the evolution

Become a founding member

Constraint CSS

lay it out like it's 1999

Soon after the W3C introduced Cascading Style Sheets, Greg Badros, the author of the Cassowary Constraint Solver & recently retired Facebook VP, proposed Constraint CSS (CCSS) as a general solution for CSS layout. Back in ‘99 Badros demonstrated responsive layouts with CCSS that today’s designers still can’t reproduce without grinding out piles of JavaScript. For more than a decade, no one seemed to take notice outside of academia until Apple implemented Cassowary & Greg’s pioneering concepts in its new AutoLayout engine with the launch of OS X Lion. Despite the evolutionary leap for app developers, web designers have had to settle with float-based & table-based layouts that have remained unimproved to this day.

“We should contemplate how very, very far behind the web platform is in making it delightful to build the sorts of things that are work-a-day in native environments.” Alex Russell

The foundation of GSS is a modernized implementation of CCSS on which we build more exotic layout APIs can and are accomplished.

Constraints, the basics

Constraints express relationships between variables that may or may not hold. Any numeric property of an element can be constrained, not just position & size.

In the following example, all paragraph tags are constrained to have line-height greater than 16px and less than 1/12th the size of the window:

p[line-height] >= 16;
p[line-height] <= ::window[height] / 12;

Constraint declaration

The syntax for declaring a constraint is as follows:

p[line-height] >= 16;

Live example

Constraints are two-way

Do not confuse an equality constraint with your everyday variable assignment. For example, in a vanilla programming paradigm:

// vanilla programming

x = y;
y = 10
x = 100;

// x => 100, y => 10

x will be 100, and y will be 10. With constraint programming, equality constraints are two-way:

// constraint programming

x == y;
x == 100;
y >= 10;

// x => 100, y => 100

x and y have a constraint to be equal, so x and y will be 100.

Constraints are incrementally added to a solver, then the solver computes a feasible & optimal solution to all the constraints. Constraint programming focuses on intentions, not implementation. This makes constraints a perfect fit for empowering declarative languages like CSS.

Regular imperative programming approach focuses on the implementation instead, making programmer solve the problems.

Constraints May or may not hold

GSS will find the best solution that satisfies the defined constraints. It uses “soft” constraints, which means that it is preferred but not required that certain constraints be satisfied.

For example:

#gss[width] == 200;
#cassowary[width] == 200;
#gss[width] + #cassowary[width] == 350;

#GSS and #cassowary elements are constrained to have width of 200px initially, and then the sum of those two is set to equal 350px. In this case, one of these two constraints will not hold. Live example.

Strengths

It is possible to influence the solution by prioritizing the constraints with strengths.

Stronger constraints completely overcome weaker ones - this is the phenomena of the Constraint Hierarchy. Strengths are declared in the same fashion of CSS’s !important:

/* Stronger constraints are more important */

#light[years] == 100 !weak;
#light[years] == 200 !medium;
#light[years] == 300 !strong;

/* #light[years] will be 300 */

There are 4 levels of built-in strength:

In a previous example, strengths assigned to constraints allow individual width constraints to overcome the sum constraint: Live example.

!require strength

!require is a special strength that guarantees the constraint will hold, otherwise everything breaks.

Pro Tip Use !require carefully & sparingly. !require’s quickly lead to systems where all required constraints cannot be satisfied in which case the GSS engine will throw an error.

For example, the following GSS constraints will be unsolvable and thus throw an error:

#gss[width] == 200 !require;
#cassowary[width] == 200 !require;
#gss[width] + #cassowary[width] == 350 !require;

We’re asking GSS to find a solution where the widths of the #gss and #cassowary element must be 200px and their sum must be 350px, which is impossible.

Constraints order

Generally speaking, later constraint declarations of the same strength are more powerful than earlier ones. There are exceptions to this behavior, as we’ll see in the following sections.

In a previous example with conflicting strengths, altered order of constraints yields a different result. Live example.

Linear Arithmetic

Cassowary, which is the constraints solving algorithm used by GSS, can only compute “Linear Arithmetic” constraints. Simple math operations like +, -, * and / are all supported, but the expressions must be linear (of the form y = mx + b). Basically, you can do everything except division and multiplication of two constrained variables. Live example.

/* this is not linear & will throw an error */

#box1[width] / #box2[height] == varX;

Selectors

Selectors are queries over a tree of HTML elements. They determine which elements are affected by the constraint.

// elements with tag name div within element with #something id
(#something div)[width] == 100;

Please reference the GSS Selectors Guide for a complete list of selector and more useful information on how to use them. *

* Note that this guide is refering to rulesets which are covered next.

Rulesets

Rulesets allow multiple constraints to be defined over a single selector

  .selector {
    width: == 100;
  }

Within a ruleset, constraints are defined on properties using the same syntax as CSS using colon : followed by a equality sign.

Rulesets can be nested:

section > article {
   .someclass {
       width: == 100;
    }
}

/* is equals to */

(section > article .someclass)[width] == 100;

CSS properties can be used in GSS rulesets:

div {
   background-color: green;
   width: <= 200;
}

Properties

Properties are variables belonging to an element. All previous examples constrained CSS properties like width or line-height. It is possible to constrain custom properties that are not recognized by CSS, but can be used in GSS constraints over known CSS properties.

When using a property known by CSS, the GSS computed value for that property will be assigned as an inline style in pixels to the element.

The & combinator refers to current element matching the ruleset selector. It is implied by the use of width: == property notation.

.container {
  width: == #elm[width];
}

/* is equals to */

.container {
  &[width] == #elm[width];
}

It is possible to constrain custom properties, that are not recognized by CSS, that can be used in constraints over known CSS properties.

.post {
  custom-property: == &[line-height] * 3;
}

/* is equals to */

.post[custom-property] == .post[line-height] * 3;

This will automatically create a custom-property for each element having a .post class. The element’s custom-property will be constrained to be three times its own line-height.

Because of the 2-way binding, changing either custom-property or line-height will affect the other property of the element.

Properties on special combinators like &, $ or ^ may omit square brackets:

body {
  .container {
    /* equate width of a <body> element with width of elements having container class */
    &width == ^width;
  }
}

Scope

Properties are attached to a specific element:

Variables prefixed with $ combinator are considered global, and can be shared accross different stylesheets.

Properties in stylesheets

Properties defined at the top level of a stylesheet will be attached to the stylesheet element. Thus each stylesheet provides its own scope. Top level properties are not directly accessible from other stylesheets.

<style type="text/gss">
  varX: == 100;
  varX: == #elm1[width];
</style>

<style type="text/gss">
  varX: == 200;
  varX: == #elm2[width];
</style>

In this example, #elm1 will have a width of 100px and #elm2 will have a width of 200px. The two varX are in different scopes and are therefore independent from each other. Live example.

Global variables prefixed with $, are accessible accross stylesheets. In the previous example, using $varX instead of varX makes constraints operate on the same variable, making the widths of two elements equal. Live example.

Adding a scoped attribute to the stylesheet attaches top most properties to the parent element of a stylesheet. If two scoped stylesheets share the parent element, they will automatically share the topmost variables.

The same scoping logic applies to external GSS files.

Properties in rulesets

Variables defined inside a ruleset define a property on each element that matches the ruleset selector.

.someClass {
  varX: == 100;
  width: == &[varX];
}

.someOtherClass {
  varX: == 100;
  width: == &[varX];

}

In this case, both rulesets define varX property for matching elements. If there’s an element that matches both rulesets, it will have both constraints applied over a single property. This may lead to a conflict specific to that element.

The latter constraint will win in case of conflict if an element has both classes from the start. Otherwise the class that was added last will overcome previous ones. Strengths can be used to define the importance of each constraint independent from order of changes. Live example.

Variables

Variables are properties that dont have explicit scope and can be accessed from inner scopes. This concept is known as lexical scoping. The scoping is determined by the source code, so it’s static and does not change during runtime.

We say that topmost scope that defines variable captures the variable, making variables in the inner scopes hoisted.

<style text='text/gss'>
varX == 100;

.className {
  width: == varX;   /* same as &width == ^varX */
  varY == &[height];

  #div {
    width: == varX; /* same as &width == ^^varX */
  }
}
</script>

If a varX was defined as a property on the top level (e.g. varX: == 100), the variable would be captured at the .className level instead.

The order of appearance or square brackets around variables have no effect over hoisting:

div {
  .rule {
    [varX] == 200; /* hoisted to parent, same as ^varX*/
  }

  [varX] == 100; /* equivalent to varX == 100; */

Variables are properties in disguise. If a variable has a name of a CSS property, it will apply the style for its resolved scope element. This can be used to define declarative constraints based on the DOM structure.

<style text='text/gss'>
varX = 100;

.wrapper {
  width == varX;

  section {
    width == &height;
  }
}
</script>

The width variable in the .wrapper ruleset captures the width variable within section ruleset. Therefore an element having wrapper class name will have its width equal to height of its descendant sections, if there are any.

Rulesets can be nested arbitrarily deep, and the hoisting reference is computed for each variable individually. A variable that was not defined in any of its parent scopes, is considered local and is identical to property.

div {
  varX == 40; // captured varX

  .someClass {
    width: == varX;

    .someNestedClass {
        varY == 45;
        width: == varY;  // captured varY
        top: == varX;
    }

    .someOtherNestedClass {
        varY == 60;
        height: == varY;  // captured varY
        width: == varX;
      }
  }
}

In this case, all nested rulesets will share the varX variable through the parent ruleset. Nested rulesets also define varY variables that are not hoisted and act like local properties. Live example.

Only variables capture other variables in nested rulesets. Anything that has explicit scope is considered a property. Properties do not capture and are not hoisted. Examples would be $global variables and &local variables. property: == notation is the cleanest way to define property in a ruleset:

div {
  varX: == 40; /* property constraint */
  varY == 45; /* variable constraint */

  .someClass {
    /* varX is not hoisted here since on the parent scope varX is a property. */
    /* A new variable varX is there created and scoped to .someClass */
    width: == varX;

    .someNestedClass {
      /* varY is hoisted from the div scope since it's scoped at that level */
      width: == varY;
      /* varX here is the .someClass varX scoped variable */
      top: == varX;
    }
  }
}

It is possible to opt out of hoisting in the same way by prefixing & to a variable name, making it a property.

div {
  varX == 50;

  .className {
    varX: == 60;
    width: == &varX; /*  60 since we are using the property varX */
  }
}

Suggested variables

Javascript code can provide numerical values that can be used in constraints. The best way to do this is to pass an object with values to the GSS constructor:

<script src="/bower_components/gss/dist/gss.js"></script>
<script type="text/javascript">
  GSS.document = new GSS(document, {"some-width": 400, "some-height": 150});
</script>

<style type="text/gss">

  section[width] == $some-width;
  section[height] == $some-height;

</style>

Suggested variables are global so they should be accessed with the $ combinator. Live example.

External variables are bound in one direction, so their values will not be affected by the solver. It is almost as if there were actual numbers in place of those variables. The difference is that these values can be updated dynamically without much effort on the solver side:

  <script>
    document.onmousemove = (e) {
      GSS.document.solve({ // make mouse movement affect variables
        "some-width": e.clientX,
        "some-height": e.clientY
      })
    }
  </script>

Position and dimension

Consult our layout guide to learn more about positioning and dimensioning elements. Having a good understanding of layout constraints will help you appreciate the next section about virtuals.

Virtuals

Virtuals are rectangles defined in GSS that don’t have a representation in the DOM tree. However regular elements can be constrained against dimensions and positions of virtuals. It makes virtuals a practical way to constrain a group of elements without the need for a DOM container element.

  "area" {
    size: == ::window[size] / 2;
    center: == ::window[center];
  }

  #elmA[top-left] == "area"[top-left];
  #elmB[bottom-right] == "area"[top-right];

In the example, both #elmA and #elmB elements are positioned against different corners of the virtual rectangle. Live example.

Since virtuals don’t exist in the DOM, applied styles have no visual effect:

"area" {
  /* Styling virtuals have no effect since they don't exist in the DOM */
  background-color: blue;

  /* Properties can be constrained, by they will not be used or inherited */
  line-height: == 20;
}

// DOM elements properties can be constrained against properties of virtuals
div[line-height] == "area"[line-height];

Virtual hoisting

Virtuals are in fact variables for rectangles. This is why virtuals follow the same scoping rules as variables. Hoisting logic applies to virtuals without explicit scope. Explicitly scoped virtuals are not hoisted and do not capture.

.className {
  /* "area" virtual is captured, and can be accessed from inner rulesets */
  "area"[size] == 200;

  #someElm > div {
    /* "area" virtual will be hoisted to element matching parent ruleset */
    width: == "area"[width];

    /* "subarea" virtual has explicit local scope and will be attached to nested divs */
    &"subarea"[width] == 200;
  }
}

Virtual splat

Virtual splats allow you to create a grid of virtuals. For example, if you want to split the viewport into four equally sized rectangles you can do the following:

"area1...4" {
  width: == ::window[width] / 2;
  height: == ::window[height] / 2;
}

"area1...2" {
  top: == 0;
  right: == &:next[left];
}

"area3...4" {
  right: == &:next[left];
}

"area1"[bottom] == "area3"[top];
"area2"[bottom] == "area4"[top];

Now any DOM element can be constrained against those four boxes. Live example.

GSS provides conditional statements for responsive design. Read our conditionals guide to learn more.

With only CCSS at your disposal, constraining common layout scenarios quickly becomes tedious. Read our VFL guide to learn how to more efficiently constrain your layout.

privacy policy