How to Add Sticky Headers with ODS HTML

View more Problem Solvers posts
This blog demonstrates how to modify your ODS HTML code to make your column headers “sticky,” or fixed in position. Using sticky headers is most beneficial when you have long tables on your web page and you want the column headers to stay in view while scrolling through the rest of the page. The ability to add sticky headers was added with CSS 2.1, with the cascading style sheet (CSS) position property and its sticky value. You might have seen this capability before CSS 2.1 because it was supported by WebKit, which is a browser engine that Apple developed and is used primarily in the Safari browser (In Safari, you use the position property with the value -webkit-sticky.) The position: sticky style property is supported in the latest versions of the major browsers, except for Internet Explorer. The FROZEN_HEADERS= option can be used with the TableEditor tagset; see the TableEditor tagset method below.

Before you start

Here is a brief explanation about the task that this blog helps you accomplish. Since the position: sticky style property is supported with the <TH> HTML tags within tables, it is very easy for you to add the position: sticky style for HTML tables that ODS HTML generates. When this CSS style attribute is added for the headers, the headers are fixed within the viewport, which is the viewable area. The content in the viewport is scrollable, as seen in the example output below.

In the past, JavaScript was the main tool for generating fixed headers that are compatible across browsers and devices. However, the position: sticky property has also made it easier to fix various other elements, such as footers, within the viewport on the web page. This blog demonstrates how to make the <TH> tag or header class sticky but enable the rest of the web page to be scrolled. The techniques here work for both desktop and mobile applications. There are multiple ways to add this style. Choose the method that is most convenient for you.

Method 1: Use the HEADTEXT= option

This example uses the position: sticky style property for the header class, which is added to the HEADTEXT= option in the ODS HTML statement. The header class is added along with the position style property between the <HEAD> and </HEAD> tags, which is the header section of the web page. This method is very convenient. However, you are limited to 256 characters and you might want to add other CSS style properties. The position style property is added using the header class name, which is used by ODS HTML to add style attributes to the column headers. As the name suggests, cascading elements cascade and enable elements with like names to be combined. In the following code example, the HEADTEXT= option uses a CSS rule with the header class and the position: sticky property for the header section of the web page.

ods html path="c:\temp" file="sticky.html"
headtext="<style> .header {position: sticky;top:0}</style>";
proc print;
ods html close;

Here is what the output looks like:

Method 2: Use the STYLESHEET= option

You can also add the position: sticky property to the header class from an external CSS file, which can be referenced in ODS HTML code by using the STYLESHEET= option with the (URL=) suboption. This method uses a CSS file as a basis for the formatting, unlike the first method above, which had applied the default HTMLBLUE style for the destination.

Another item worth mentioning in this second example is the grouping of the CSS class selectors, which match the style element names used with ODS and the TEMPLATE procedure. For example, the body, systemtitle, header, rowheader, and data class selectors are added and grouped into the font-family style property. This method is also used for several of the other style properties below. The data class adds some additional functionality worth discussing, such as the use of a pseudo style selector, which applies a different background color for even alternating rows. In the example below, the class names and the template element names are the same. You should place the CSS style rules that are shown here in a file that is named sticky.css.

.table tbody tr:nth-child(even) td { 
background-color: #e0e0e0; 
color: black;
.body, .systemtitle, .header, .rowheader, .data { 
font-family: arial, sans-serif; 
.systemtitle, .header, .rowheader { 
font-weight: bold
.table, .header, .rowheader, .data { 
border-spacing: 0; 
border-collapse: collapse; 
border: 1px solid #606060;
.table tbody tr:nth-child(even) td { 
background-color: #e0e0e0; 
color: black;
.header { 
background-color: #e0e0e0;
position: -webkit-sticky;
position: sticky;
.header, .rowheader, .data { 
padding: 5px 10px;

After you create that CSS file, you can use the ODS HTML statement with the STYLESHEET= option. In that option, the (URL=) suboption uses the sticky.css file as the basis for the formatting. Forgetting to add the (URL=) suboption re-creates a CSS file with the current template style that is being used.

ods html path="c:\temp" file="sticky.html"
proc print;
ods html close;

Here is what the output looks like:

The pseudo class selector in the CSS file indicated that even alternating rows for all <TD> tags would be colored with the background color gray. Also, the position: sticky property in the header class fixed the position of the header within the viewport.

Method 3: Use the TableEditor tagset

A third method uses the TableEditor tagset, which enables sticky headers to be added by using options. Options are also applied to modify the style for the alternating even and odd rows as well as to have sortable headers.

/* Reference the TableEditor tagset from */
filename tpl url "";
/* Insert the tagset into the search path for ODS templates. */
ods path(Prepend) work.templat(update);
%include tpl;
ods tagsets.tableeditor file="c:\output\temp.html" 
banner_color_even="#e0e0e0") style=htmlblue;
proc print;
ods tagsets.tableeditor close;

Here is what the output looks like:

In summary, this article describes how easy it is to add sticky headers to tables that are generated by using the ODS HTML destination. Adding fixed headers to any table allows the output to dynamically preserve the headers in the viewable area while scrolling through the table, allowing a much richer experience. Give it a try and let me know how it goes.

Learn More


About Author

Chevell Parker

SAS Technical Support Analyst

Chevell is a Senior Principal Technical Support Analyst in the Foundation SAS group within SAS Technical Support. His main support areas include the Output Delivery System and XML technologies.


  1. This is fabulous, Chevelle! Is there any way to make the first two header rows sticky? I have a table that uses spanned column headers. When I make the headers sticky, it is only showing the second row. Thank you!

  2. Hi Chevell,

    This is a great example-driven article, thank you so much. I have been trying to give my users a good excuse use the ODS HTML destination and not just export output to Excel simply to get the frozen headers feature.

    Now, having achieved the impossible, are you able to point us in the direction of how we can freeze the Titles of the Proc PRINT as well ?

    Having the Titles and the table column headers frozen is going to make for a really useful reporting tool. Is this possible using a tweak to what you have already.

    Many thanks,

    Downunder Dave

    • Chevell Parker
      Chevell Parker on

      I am glad you like it Robert and also happy to hear you are able to find a place for it along with all of those amazing graphs that you generate!

Leave A Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Back to Top