Wednesday, April 08, 2015

CSS tricks for (conditional) formatting of numbers and dates

Here's a bunch of CSS tricks that can help to format numbers and dates in HTML. You can even use it to achieve (basic) conditional formatting!

A Stackoverflow question: conditionally hiding zero values in a table

Today I stumbled upon this question on stackoverflow:
Is there a way to hide a data cell based on a specific value using just HTML/CSS? For example I have this code:
<table>

 <caption>Test</caption>

 <tr>
  <th>Values</th>
  <td>$100</td>
 </tr>

 <tr>
  <th>Initial value</th>
  <td>$0</td>
 </tr>

</table>
Is there a way to hide the cells that are equal to $0 using HTML/CSS only? Let's say instead of $0 I have a variable called fee that can be a variety of values: $0, $5, $20, etc. Is there a way to check what value it is and if it is found to be $0 can I then hide it?
As it turns out, this is actually possible with HTML5 data attributes, the CSS :before or :after pseudo-class, a CSS content property using a value of the type attr(), and attribute-value selector syntax to control conditional formatting:
<!doctype html>
<html>

 <head>

  <style type="text/css">
    
    /* make the cells output the value of their data-value attribute */
    td:after {
      content: attr(data-value);
    }
    
    /* hide the output if the data-value is equal to "$0" */
    td[data-value="$0"]:after {
      content: "";
    }

  </style>

 </head>

 <body>

  <table>

   <caption>Test</caption>

   <tr>
    <th>Values</th>
    <td data-value="$100"></td>
   </tr>

   <tr>
    <th>Initial value</th>
    <td data-value="$0"></td>
   </tr>

  </table>

 </body>

</html>
In summary, the ingredients of the solution are:
  • Encode the cell values as a custom data attribute, for example: data-value. The actual cells are empty.
  • Make the cell value show up using the :after pseudo-class of the td element. This is done by setting the CSS content property to the value attr(). Values of this type take an attribute name between the parenthesis, so in our example this becomes attr(data-value).
  • Use the attribute-value selector syntax for conditional formatting. In our example the requirement was to "hide" the value of cells with an amount equal to "$0". We can express this as td[data-value="$0"]. And since we display the value through the content property of the :after pseudo-class, we have to add :after to our td selector and specify a content property of "" to override the previous rule that outputs the value using attr().
The result looks something like this:

Values $100
Initial value


Browser compatibility

When I first tried to implement the idea I tested with latest chrome (41) and firefox (37) where it worked just fine. Much to my surprise and joy, it works without modification in IE8 as well! I'm so happy that I don't want to spoil it by testing other IE versions, but if anyone dares to try then I'd be grateful if you could post the result in the comments. Now personally, I'm not interested in hiding cell values. But this little trick does offer some possibilities for some basic conditional formatting.

Monetary amount formatting: red vs black

Consider a balance sheet of monetary amounts. Amounts should be right aligned, and we want the positive amounts to be displayed in black, and negative amounts in red:
<!doctype html>
<html>

 <head>

  <style type="text/css">
    
    /* right-align monetary amounts */
    td[data-monetary-amount] {
      text-align: right;
    }

    /* make the cells output their value */
    td[data-monetary-amount]:after {
      content: attr(data-monetary-amount);
    }

    /* make debit amounts show up in red */
    td[data-monetary-amount^="-"]:after {
      color: red;
    }
    
  </style>

 </head>

 <body>

  <table border="1">

   <tr>
    <th>Gain</th>
    <td data-monetary-amount="$100"></td>
   </tr>

   <tr>
    <th>Losst</th>
    <td data-monetary-amount="-$100"></td>
   </tr>

  </table>

 </body>

</html>
Note the data-monetary-amount^="-" syntax. This is a so-called substring matching attribute selector, which is specified in CSS 3. The comparison operator ^= tests whether the attribute value starts with a particular string, in this case the minus sign "-", which indicates we have a negative amount.

CSS 3 specifies similar comparison operators for a postfix match ($=) and an instring or "mid" match (*=).

The result looks something like this:

Gain $100
Loss -$100

Browser compatibility

As if I hadn't been blessed enough, this solution too works in IE8 (as well as Chrome and Firefox of course). Yay!

A slightly less nice solution that works in CSS 2.1

You can achieve the same effect with CSS 2.1 if you encode the value in 2 data attributes, one for the sign and one for the actual absolute amount:
<!doctype html>
<html>

 <head>

  <style type="text/css">

    /* right-align monetary amounts */
    td[data-monetary-amount] {
      text-align: right;
    }

    /* make the value show up */
    td[data-monetary-amount]:after {
      content: attr(data-monetary-amount);
    }

    /* make negative amounts show up in red, prefixed by the sign */
    td[data-sign="-"]:after {
      color: red;
      content: attr(data-sign) attr(data-monetary-amount);
    }

  </style>

 </head>

 <body>

  <table border="1">

   <tr>
    th>Debit</th>
    <td data-sign="+" data-monetary-amount="$100"></td>
   </tr>

   <tr>
    th>Credit</th>
    <td data-sign="-" data-monetary-amount="$100"></td>
   </tr>

  </table>

 </body>

</html>
An interesting bit of this last example is that it shows you can compose the value of the content property out of multiple pieces of content, in this case, two attr() values: attr(data-sign) to ensure that in case of negative values, we display the minus sign, and attr(data-value) to output the absolute value of the amount.

Locale dependent date formatting

The elements we saw in the previous example can be used for basic locale dependent date formatting. Let's keep it simple and format dates either in USA format, mon/d/yyyy, or in a format that is more easily understood outside the USA, d/mon/yyyy:
<!doctype html>
<html>

 <head>

  <style type="text/css">
      /* year comes last */
      time[datetime]:after {
        float: right;
        content: attr(datetime);
      }

      /* month and day come first */
      time[datetime*="-"] {
        float: left;
      }

      /* Months (non-USA) */
      time[datetime^="01-"]:after {
        content: "jan/";
      }

      ...rules for the other months go here...

      time[datetime^="12-"]:after {
        content: "dec/";
      }

      /* Days (non-USA) */
      time[datetime$="-01"]:before {
        content: "1/";
      }

      ...rules for the other days go here...

      time[datetime$="-31"]:before {
        content: "31/";
      }

      /* Months (USA) */
      *[lang="en-US"] time[datetime^="01-"]:before {
        content: "jan/";
      }

      ...rules for the other months go here...

      *[lang="en-US"] time[datetime^="12-"]:before {
        content: "dec/";
      }

      /* Days (USA) */
      *[lang="en-US"] time[datetime$="-01"]:after {
        content: "1/";
      }

      ...rules for the other days go here...

      *[lang="en-US"] time[datetime$="-31"]:after {
        content: "31/";
      }

  </style>

 </head>

 <body>

  <table border="1">

   <tr>
    <td lang="en-US">
     <time datetime="2015">
      <time datetime="04-08"/>
     </time>
    </td>
   </tr>

   <tr>
    <td lang="en-GB">
     <time datetime="2015">
      <time datetime="04-08"/>
     </time>
    </td>
   </tr>

  </table>

 </body>

</html>
This solution uses the HTML5 time-element. The time element can have a datetime attribute that contains the date/time in a machine readable format, and it may contain text content, which should be a human-readable representation of the date/time.

Now, personally, I do not think the HTML5 time element is an example of good or convenient design. At least, not from the perspective of the HTML5 author.

It is a great idea to require a machine-readable representation of the date. This potentially allows user agents to do useful things with the content. And allowing the user to manually specify the human-readable representation is also not a bad idea per se. But IMO, the time-element would have been much more useful if authors would be allowed to only specify the machine-readable representation of the date/time and, in absence of a manually entered human representation of the date/time, let the browser figure out how that date appears in the output in a human-readable representation. That way the browser could use information about the language of the document or document section to auto-format the date, or otherwise apply some user-preference. Another idea would be to allow the HTML author to control the output format using another attribute for a format string.

Anyway, this is not the case so we can try and see what we can do on our end. The solution above is as follows:
  • In the example above, a date is expressed using two time elements: one for the year-part and one for the month and day parts of the date. The year-part uses a non-negative integer for the datetime attribute, indicating a year. The mont/day-part uses a datetime attribute to represent a valid yearless date string. I nested the time element that represents the month and day part inside the one that represents the year. That said, it would have been much nicer if I could've just used a single time-element using a single datetime attribute containing all dateparts, but I couldn't figure out how to manipulate such a value with CSS. So I settled for a less optimal solution, which is certainly more verbose. At least, it does not duplicate any data, which seems a requirement that we never should let go off.
  • The first two CSS rules ensure that month and day appear first (using float:left) and the year appears last (using float: right). The first CSS rule specifies that all time elements having a datetime attribute should float right. The way we set it up, this matches the time elements that match the year part. The second CSS rule uses the substring-matching attribute selector *= to check if the datetime attribute of the time element contains a hyphen. Since the hyphen separates the day and month parts in the yearless date string format, this rule will match all time elements that represent a month/day part of a date.
  • The remaining rules are required for formatting the month and date parts as well as the separators. (Wich is a slash, /).
  • The prefix matching attribute selector ^= is used to test which month is identified by the prefix of the value of the datetime attribute. For each month, with prefixes 01 through 12, there is a rule, and its content property is used to output the month abbreviation like jan, feb, mar etc.
  • The postfix matching attribute selector $= is used to test which day is identified by the postfix of the value of the datetime attribute. For each day, with postfixes 01 through 31, there is a rule, and its content property is used to output the day number.
  • The upper set of rules matching the month-prefix and day-postfix are used to generate :after and :before pseudo-classes respectively to ensure that by default, the day part is displayed before the month part.
  • To accommodate the USA date format, the bottom set of rules was added. These are essentially a duplication of the prefix- and postfix matching rules for the month and day part respectively, but these rules have an initial selector part like this *[lang="en-US"] to ensure that these rules are active only if the time element is contained in a section that was marked as being localized for the USA. For these rules, the month parts are used to generate :before pseudo-classes, and the day parts are used to generate :after pseudo-classes, thus reversing the default order of displaying the month and day part.
The result looks something like this:

apr/8/2015
8/apr/2015

Browser compatibility

This solution works again fine in Chrome and Firefox, but does not render in IE8. Support for the time element was added in IE9, and the example works just fine there. Of course, if you really want it to work for IE8, you can, just don't use a time element but something generic such as span, and use a custom data- attribute for the datetime value, like data-datetime or similar.

Finally...

I hope this was useful information. Personally I think we still have a long way to go before we can use a pure css solution to solve all our data formatting problems, and I believe that esp. for web-applications, programmatic solutions (either on the server or on the client side) are still essential to deliver an acceptable result.

That said, every little bit of functionality in CSS can help you build a solution, even if such a solution is still controlled or generated by a programmatic backend.

Any comments and feedback are greatly appreciated.

9 comments:

Unknown said...

One question: How can the output appear in the final webpage as selectable text? It seems after my table appears with this technique, all the text is not selectable nor copyable to other places. Not sure why that is.

rpbouman said...

@Sevron Deeps,

that's a great point! I never considered this issue - shame on me. Unfortunately it looks like the text simply not selectable because the values are rendered using pseudo elements (:after). Pseudo elements are not actually part of the DOM, and I believe that makes the browser consider the text not selectable.

Here's a source https://www.w3.org/TR/CSS2/selector.html#pseudo-elements

This is actually a major problem for my hack :(
Thanks for pointing it out. I will need to reconsider and see if I know some way out of this.

Sean Flatley said...

Hey Roland,

Thanks for sharing. Was looking for a way to conditionally change the background color of a div.

Hope you are doing well.

rpbouman said...

Hi Sean!

Hey, glad you found this. I hope it helps!

I'm ok. How have you been? I hope all is well :)

Warm regards,

Roland.

Manfaat2 said...

thanks for the information

Anonymous said...

Thanks -Issue I find is that if you apply the CSS the table "sortable" function seems to not take the values as values i.e. biggest to smallest etc. Wonder why?

rpbouman said...

@Anonymous,

where does your "sortable" function come from? It is not a standard thing. Did you get it from some framework, like jquery plugin?

Most likely reason is that it does not, by default, look at the data attribute for the value to sort. You're going to have to modify your sorting function to work with data attributes.

HTH

Unknown said...

Mr. Bouman. Thanks for sharing this. Truly appreciated!

Tria said...

i will try this.

DuckDB bag of tricks: Processing PGN chess games with DuckDB - Rolling up each game's lines into a single game row (6/6)

DuckDB bag of tricks is the banner I use on this blog to post my tips and tricks about DuckDB . This post is the sixth installment of a s...