Dynamic CSS with DynCSS

By Vittorio Zaccaria

DynCSS is a small Javascript add-on for your web pages. It parses your CSS and converts all the rules with prefix -dyn-* into dynamic Javascript that is executed on browser’s events like scroll and resize.

For example, the following CSS will center vertically .header, dynamically changing margin-top as the window is resized:

.header {
 -dyn-margin-top: '(@win-height - @el-height)/2.0';
}

The dynamic action is specified with simple quotes, and it can use placeholders to refer to dynamic properties in the current browser window. In the above example, @win-height is the dynamic height of the window while @el-height is the dynamic height of the current element (.header in this case). The overall effect is that, every time the window is resized, the .header will automatically re-center.

The final product of this tutorial

In this tutorial we will walk through building a landing page with DynCSS. The final version of this landing page can be found at this address. As you can see, it contains several effects and animations. In this article, I’ll describe how I achieved those effects.

A Word on Responsiveness.

DynCSS allows you to easily program responsiveness within your CSS. To do so, you define a list of breakpoints and the dynamic variable to watch (which is typically the size of the window). You can do this in a <script> tag in your HTML. Here’s how you set two breakpoints at 481 and 961 on the window’s width variable @win-width:

<script>
    window.dynCss.api.setBreakpoints([481,961],'@win-width');
</script>

Now, you can define how each property behaves above and below the breakpoints. For example, we can dynamically change the font size:

.header__title {
        -dyn-font-size: '@selectFrom(["3em", "4em", "5em"])';
}

selectFrom is a built-in function that returns one of the elements from the input list by using the breakpoints specified with setBreakpoints. Here, the font size will be 3em under 480px, 4em under 960px, and 5em above 960px.

For this tutorial we will use only one breakpoint at 480px:

<script>
    window.dynCss.api.setBreakpoints([481],'@win-width');
</script>

In this case, the @selectFrom will specify a two element array containing the value that the property will assume above and below that breakpoint.

The Landing Page Above the Fold.

As you ‘land’ to the page you will see:

  • a centered header;
  • a phone positioned on the bottom right;
  • a chevron-like arrow pointing to the phone;
  • a set of 4 scroll spies positioned vertically on the left.
landing

Let’s examine the behavior of these elements.

The Header

To center the header in the page we specify how the margins should be computed given the size of the header itself:

.content__page1 {
 -dyn-margin-top    : '(@win-height - @el-height)/2.0';
 -dyn-margin-bottom : '(@win-height - @el-height)/2.0';
 -dyn-margin-left   : '(@win-width - @el-width)/2.0';
}

If you scroll down the page, you will see that the header will fade; this is achieved using this rule that acts dynamically on the opacity:

.content__page1 {
  -dyn-opacity: '@convergeToZero({when : (@win-scrollTop - (@jq-position.top)), isHigherThan : @win-height/2})';
}

Here, @convergeToZero is a built-in function that returns values from 0 to 1, win-scrollTop is the scrolling position and jq-position.top evaluates — with jQuery — the position of the element (implicitly equivalent to .content__page1). When the scrolling position is above the element, the opacity is 1; as the difference between the scrolling top and the element’s top increases, the opacity is turned gradually down and will be set to zero when the difference is above half of the window’s height.

The Phone

The phone image will undergo several transformations; first its vertical position is going to change linearly from @win-height+100 to the vertical center of the window as the scroll top moves from zero to the top of the second page.

phone1

To do that, we set the element’s position to fixed and use the virtual property fixed-vertical-center that is translated dynamically into changes to the top and left properties of the element:

.phone-preview__phone {

  position: fixed;

  -dyn-fixed-vertical-center: '@morph(@transitionToOne({when : @win-scrollTop , start: 0 , stop: @pos(".content__page2").top }, .5) , @win-height + 100 , @fixedVerticalCenter(window))';
}

You can see above three additional built-in functions:

  • @pos(element) evaluates the absolute position of .content__page2 in the document.
  • @transitionToOne({when, start, stop}) signals when the scroll top has reached the top of .content__page2
  • @morph(c, v1, v2) returns a convex combination of v1 and v2 by using the value of c.

The overall effect is an animation controlled with the scrolling position.

Other animations using the same phone are done by manipulating its horizontal center, its opacity and its css rotation transform:

.phone-preview__phone {

  -dyn-fixed-horizontal-center: '@morph(@transitionToOne({when : @win-scrollTop , start: 0, stop: @pos(".content__page2").top }, .5) , @win-width*3/4    , @fixedHorizontalCenter(window))';

  -dyn-transform: '"rotate(#{@morph(@transitionToOne({when : @win-scrollTop , start: @pos(".content__page2").top + @win-height * .1 , stop: @pos(".content__page3").top }, 1), 0, 270)}deg) "';

  -dyn-opacity: '1-@transitionToOne({when : @win-scrollTop , start: @pos(".content__page3").top + @win-height * .1 , stop: @pos(".content__page4").top - @win-height/2 }, 1)';
}

Here’s a snapshot of when the transform animation has kicked in:

phone2

We also manipulate its display property to make it disappear when the scroll position reaches the fourth page:

.phone-preview__phone {

  position: fixed;

  -dyn-display: '@if(@transitionToOne({when : @win-scrollTop , start: @pos(".content__page3").top + @win-height * .1 , stop: @pos(".content__page4").top - @win-height/2 }, 1) < 0.9, "", "none")'
}

In this example, @if(c, v1, v2) returns v1 if c is true, v2 otherwise. The effect is that the phone will disappear when the scroll top reaches a bit less than the top of the fourth page.

The Chevron Arrow

The chevron arrow is positioned very simply by setting its bottom edge to the top edge of the phone and by aligning its horizontal center with the one of the phone:

chevron

You can also note that we disable it when the window size is less than the breakpoint that has been set earlier in the article (-dyn-display property):

.phone-preview__chevron {

  position: fixed;
  -dyn-fixed-bottom-edge: '@fixedTopEdge(".phone-preview__phone")';
  -dyn-fixed-horizontal-center: '@fixedHorizontalCenter(".phone-preview__phone")';
  -dyn-display: '@selectFrom(["none", "block"])';
}

The Scroll Spy

The scroll spy on the left appears only on desktop browsers:

.position-spy {
 position: fixed;
 -dyn-display: '@selectFrom(["none", "block"])';
}

Its center is positioned at 1/6 of the window width and 1/2 of the window height:

.position-spy {
  -dyn-fixed-vertical-center: '@fixedVerticalCenter(window)';
  -dyn-fixed-horizontal-center: '@win-width/6';
}

Each of the inner spies can be extended with a .highlight class that adds a white fill inside it.

spies

Setting the .highlight class based on the scroll position is very simple using the DynCSS -dyn-set-state-(class) virtual property.

This rule, for example, adds the .highlight class to .position-spy__page1 only when the scroll position is within 90% of the first page:

.position-spy__page1 {
  -dyn-set-state-highlight: '@if(@transitionToOne({when : @win-scrollTop , start: 0, stop: @pos(".content__page2").top }, .5) < 0.9, true, false)';
}

Using a Preprocessor

By sprucing up a little bit the CSS I’ve been able to build this neat landing page with minimal effort. Using a CSS preprocessor,it can be even easier to create complex and responsive pages. For example, using LESS’ interpolation, you can write comfortably complex animations:

@page2-start:           '@pos(".content__page2").top';
@trans1-limits:         'start: 0, stop: @{page2-start}';
@animationStatus1:      '@transitionToOne({when : @win-scrollTop , @{trans1-limits} }';
@convergeHorizontally:  '@morph(@{animationStatus1} , @win-width*3/4, @fixedHorizontalCenter( window ))';

.phone-preview__phone {
  -dyn-fixed-horizontal-center : '@{convergeHorizontally}';
}

Wrapping up

DynCSS was born with the intention of make it easier to specify complex animations that go beyond to what it is/will be possible with CSS’s calc. Although it is in its infancy, I find it already very comfortable to use and I am looking forward to pull requests to the repository!

Previous

Introduction to the Ambient Light API

Building Your First Grunt Plugin

Next

2 thoughts on “Dynamic CSS with DynCSS”

Comments are closed.