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 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 designer's 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. From CCSS primitives other, more exotic layout API's can and are accomplished.

Constraints, the basics

Constraints express relationships between variables that may or may not hold. You can constrain any numerical style property of an element, not just its position & size. For example, if I want all paragraph tags 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;

or, we could write it as a single chained statement:

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

or, we could express the same constraints within a CSS ruleset alongside old-school CSS like so:

p {
  color: purple;
  line-height: >= 16;
  line-height: <= ::window[height] / 12;  
}

of course, you can nest rules too:

.content {
  
  margin-right: == ::parent[margin-right] * -1;
  
  p, blockquote {
    color: purple;
    line-height: >= 16;
    line-height: <= ::window[height] / 12;  
  }
}

Numbers are treated as pixel values except for properties like:

.sprite {
  z-index: >= #bg[z-index] >= 50;
}

Constraints are 2-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, so:

// 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. With constraint programming you input the what you want to happen, not how it will happen. With procedural programming, you focus on the how. This makes constraints a perfect fit for empowering declarative languages like CSS.

Custom Constraint Variables

A custom constraint variable can be created by simply using it in a constraint.

.post {
  width: == 6 * [col-size];
}

This automatically defines the constraint variable [col-size] that can be re-used in other constraints.

Custom Element Constraint Variables

You can create a custom variable for an element by, again, simply using it:

.post[col-size] == .post[line-height] * 3;

This will automatically create a [col-size] variable for each element with the class post, and constrain it to be the same as 3 times that post's line-height. Don't forget this relationship is 2-way, so further constraining a post's [col-size] can affect its line-height, which can be remedied using by taking advantage of the "Constraint Hierarchy" with Strengths or Stays.

Global Scope

Constraint variables are defined in a global scope, as is right in the constraint-based world. So, all uses of [col-size] below refer to the same constraint variable.

/* the !require strength ensures this constraint will hold */
[col-size] == ::window[width] / 12 !require; 

.menu {
  width: == [col-size];
}

.post {
  width: == 6 * [col-size];  
  h1 {
    padding-left: == [col-size];
  }
}

Hardware Accelerated Layout Variables

By default, GSS uses Matrix3D transforms to hardware accelerate the browsers that can. The following position properties are treated specially:

Special Pseudo Selectors

The this and parent selectors can only be used in a nested ruleset, as with:

.box {
  width: == ::parent[width];
  line-height: == ::[width] / 12;  /* same as ::this[width] */
}

PRO TIP: parent selectors can be used to "bridge" variables in interesting ways. Also a recommended practice is to prefix custom element variables with $ to avoid confusion with native CSS properties. A contrived example:

#product {
  
  $col-width: == ::[width] / 12 !require;
  $col-left: == ::[left] + ::[$col-width];
  
  .price {
    left:  == ::parent[$col-left];
    width: == ::parent[$col-width];
  }
  
  .description {
    left: == ::parent[$col-left] + ::parent[$col-width];
  }
}

Intrinsic Properties

What if we want an paragraph's height to be the intrinsic height of its text content? We use an intrinsic property!

/* all paragraphs will have a height equal to its text-height */

p {
  height: == ::[intrinsic-height];
}

When you add intrinsic- before a property, GSS will try to measure that property from the DOM every time it needs to solve for new values, as when a paragraph text-content changes or when the window size changes & you used ::window somewhere. GSS uses a deferred run loop to avoid serial DOM read / writes, but reading the DOM can always cost perf.

PRO TIP: minimize use of intrinsic properties. For example, best to hardcode image width & height in equality constraints if can be known ahead of time.

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 completely overcome weaker ones */

#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:

All constraints have a strength. !weak is the default for all constraints.

!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.

Weights

Constraints of the same strength can be made relatively stronger or weaker with weights, like so:

/* Constraints of the same strength... */

#planet[weight] == 600 !medium1000; 
#planet[weight] == 30 !medium;
#planet[weight] == 10 !medium;
#planet[weight] == 20 !medium;

/* #planet[weight] will be 600 */

#planet[weight] will be 600. The first constraint has an additional weight of 1000 used in calculating its potential error, making not satisfying it more costly.

Ambiguity

bottom & right are expressions of position & size, which can lead to ambiguous constraints like:

#box[right] == ::window[right];

/* this is too ambiguous without constraining #box[width] or #box[left] */

Should the box move its position to the right or stretch its width to make its right edge flush with the window? A critical best practice is to fully constrain an element, in other words, you should adequately specify an element's position & size. If an element is under-constrained you will have unpredictable results.

Stays

Ambiguity can be largely overcome by declaring more constraints, but what about situations like:

#box[left]  >= 100;
#box[width] >= 100;
#box[right] == ::window[right];

/* the #box's width will stretch */

Will the #box move or stretch to satisfy the last constraint? Again, the earlier constraint statements are more powerful than later ones, so the box's width would stretch. You can control this by either changing the order in which the constraints are declared or by using a stay like so:

#box[left]  >= 100;
#box[width] >= 100;
@stay(#box[width]);
#box[right] == ::window[right];

/* the #box's position will move */

Many stays can be declared at once with @stay([var1],[var2],[var3]).

Minimized Total Error

For constraints at the same strength, GSS tries to minimize the degree to which all constraints are not satisfied. Internally, Cassowary finds what's called a "locally-error-better", "weighted-sum-better" solution to constraints. As an example,

/* Constraints of the same strength... */

#laser[amo] == 1 !strong;
#laser[amo] == 2 !strong;
#laser[amo] == 3 !strong;

/* #laser[amo] will be 2 */

#laser[amo] will be 2 because that minimizes the total degree to which all constraints would not be satisfied, or the total of each constraint's "error".

When #laser[amo] is 2; the first constraint has error of 1, the second has an error of 0 & the third has error of 1, total error = 2.

If #laser[amo] was 3; the first constraint has error of 0, the second with 1 & the third with 2, total error = 3.

The total error is always minimized.

Linear Arithmetic

Cassowary only allows for "Linear Arithmetic" constraints, you can do simple math operations like +, -, * and /, but all expressions must be linear (of the form y = mx + b). Basically, you can do most things except multiply or divide two constraint variables.

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

#box1[width] / #box1[height] == #box2[width] / #2box[height];

I wish we could go non-linear, but we can't because math.