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:
Constraint declaration
The syntax for declaring a constraint is as follows:
p[line-height] >= 16;
p
is the selector. This constraint will apply to everyp
tag in the page.[]
is the property accessor syntax.line-height
is the property for which a value will be calculated by GSS.>=
defines an inequality constraint.16
is the numerical value for the constraint in pixels (the default unit of measurement in GSS).
Constraints are two-way
Do not confuse an equality constraint with your everyday variable assignment. For example, in a vanilla programming paradigm:
x will be 100, and y will be 10. With constraint programming, equality constraints are two-way:
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
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
:
There are 4 levels of built-in strength:
!weak
!medium
(default)!strong
!require
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:
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.
Selectors
Selectors are queries over a tree of HTML elements. They determine which elements are affected by the constraint.
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
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:
CSS properties can be used in GSS rulesets:
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.
It is possible to constrain custom properties, that are not recognized by CSS, that can be used in constraints over known CSS properties.
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:
Scope
Properties are attached to a specific element:
- In rulesets, it’s an element that matches the selector.
- In the topmost level of a stylesheet, it’s a
<link>
or<style>
that defines the stylesheet. - In a stylesheet element with the
scoped
attribute, topmost properties are attached to the parent element of a stylesheet.
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.
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.
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.
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:
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.
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.
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:
It is possible to opt out of hoisting in the same way by prefixing &
to a variable name, making it a property.
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:
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:
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.
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:
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.
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:
Now any DOM element can be constrained against those four boxes. Live example.
Read next
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.