Skip to content

Adding custom CSS

marksreeves edited this page Jan 27, 2016 · 24 revisions

There are several ways to add custom CSS to one's WComponents application. Under most circumstances this should be unnecessary as the theme should provide a common look and feel for the application suite.

Some reasons for custom CSS

Usually the theme should provide all the necessary styling, the following are some reasons why one may need to add custom CSS for a view:

  1. responsive design as this is not really possible in an abstract framework as it requires knowledge of the application's layout to determine where items can be compressed, re-ordered or removed;
  2. local changes to individual instances of a control;
  3. user choice.

Order of precedence

The CSS applied in a WComponents application is attached in the HTML output stream in the following order:

  1. base theme CSS is added to the head element; then
  2. platform and browser specific CSS is added to the head element; then
  3. HTML link elements added to any container component in the application in their order in the source tree are output into the head element; and
  4. CSS links created using ui:css /WApplication.addCssUrl or WApplication.addCssFile are added to the head element; then
  5. HTML style elements added to any container component in the application in their order in the source tree are output in-situ (at the moment) wherever they are in the input stream.

Note that #5 above is under eternal review as there are competing interests of XSLT performance and a preference to have all style elements which do not have a scoped attribute set placed into the head element where they belong. If we eventually hoist unscoped style elements to the head they will be placed after the application specific link elements.

Adding a cacheable stylesheet

The most efficient way to add CSS is usually using a cacheable stylesheet. It is not difficult to add one of these, what it should contain is another matter and is dealt with below.

Using WApplication

This is new and not yet in a released version of WComponents (release 1.0.2). If you build from source you will have this method available to you.

WApplication has these methods which may be used to add a link element to the page head element:

  • addCssUrl(String url); and
  • addCssFile(String fileName).

Each of these methods has the same net effect: a HTML link element will be added to the head linking to the url or file resource.

// Get the WApplication from the current object'
WApplication app = WebUtilities.getAncestorOfClass(WApplication.class, this);

// Then use this WApplication to add CSS from a resource.
app.addCssFile("/path/to/resource/my_css_file.css");
// Adding from a URL is similar. Use '//'' to avoid defining the protocol.
app.addCssUrl("//example.com/path/to/static_css_file.css");

Using Headers

This method will be deprecated in favour of that outlined above but is available in WComponents 1.0.2 and before.

A CSS stylesheet can be added to an application using com.github.bordertech.wcomponents.Headers.addUniqueHeadLine(String type, String aLine) or com.github.bordertech.wcomponents.Headers.addUniqueHeadLine(String aLine).

The advantage of this is that the CSS will be downloaded by the browser and potentially cached (dependent on user settings).

We are in the process of adding methods to WApplication to make the process of adding a CSS file link less obscure. In the meantime one possible method of doing this could be by using WContent and an internal resource :

private final WContent css = new WContent();

// ...

// Add a cache key to css so that it can be cached. This cache key
// can then be changed if there is a need to update the CSS.
css.setCacheKey("my.custom.css" + version);

// The resource must be added to the output tree event though it is
// not included in the XML output.
add(css);

// ...

// At some point the css resource must be set up. This could be the
// first time the object to which css was 'added' is painted in
// preparePaintComponent
@Override
protected void preparePaintComponent(final Request request) {
    super.preparePaintComponent(request);

    // ...

    if (!isInitialised()) {

        // ...

       css.setContentAccess(new InternalResource(
                "/path/to/resource/my_custom_stylesheet.css",
                "my_custom_stylesheet.css"));
        setInitialised(true);
    }

    // ...

    // Finally use addUniqueHeadLine To attach the css
    UIContext uic = UIContextHolder.getCurrent();
    uic.getHeaders().addUniqueHeadLine(
        "<link type='text/css' rel='stylesheet'" +
        "href='" + WebUtilities.encode(css.getUrl()) + "'/>");
}

Using WText to add a link to a stylesheet

WARNING this could be considered a dirty hack and your particular theme may not support it, but it will work in the default theme.

If the stylesheet is at a fixed URL it may be able to add it to the application by building a HTML link element. To do this one could use WText as per the example below:

// NOTE that the link element must be XML compliant so must be self closing.
private static final String CSS_LINK_CONTENT =
    "<link type='text/css' rel='stylesheet'" +
    " href='//example.com/my_custom_stylesheet.css' />";

private final WText cssLink = new WText(CSS_LINK_CONTENT);

// ...

// Somewhere we need to turn off text encoding and add the WText.
cssLink.setEncodeText(false);
add(cssLink);

This will write a HTML link element to the output stream. The default theme XSLT will hoist this link into the HTML head element.

The main downside of this method (and why it is a dirty hack is that one has to be aware of what the consequences will be of adding a WText with no UI artefact to any given container. If the WText is added to a WPanel layout it will reserve a cell for the WText. This may result in unexpected alignment or spacing. If, for example, the WText is added to a ColumnLayout then it will result in an empty cell in the row and column into which the WText is added. There are simple techniques for avoiding this.

Using a template

As at January 2016 WComponents supports Velocity templates through a package protected method exposed in WContainer (and therefore WPanel). We are working on a replacement for this which should make adding custom templates easier and more efficient. If a WPanel has a Velocity template then that template is used to render the panel, not its XML renderer. All components added to the WPanel have to be tagged for inclusion in the velocity template.

It would probably be overkill to create a WPanel with a velocity template just to add a CSS link but it is feasible.

Adding inline CSS

Sometimes a little bit of CSS has to be added to particular renderings or user contexts. We are also working on ways to make this easier but the current method is to use WText or a Velocity template.

An example of using WText is shown below. A much more convoluted example is in the wcomponents-examples com.github.bordertech.wcomponents.examples.theme.WRowExample.addAppLevelCSSExample().

private final WText cssText = new WText();

// These do not have to be static, or even defined, they are here to make reading
// this rubbish a bit easier.
private static final String STYLE_START = "<style type='text/css'>";
private static final String STYLE_END = "</style>";
private static final String MY_CSS = "{font-size:3em;}";

// ...

// Add the WText to the output tree.
add(cssText);

// ...

// in preparePaintComponent during initial paint so we have access
// to the component ID:
cssText.setText(STYLE_START + "#" + getId() + MY_CSS + STYLE_END);
cssText.setEncodeText(false);

If the component being styled has has its htmlClass property set then this can be used as a selector instead of an ID. This has a slight advantage over using an ID in that the htmlClass is not changed by inclusion in a naming context whereas a components ID is. The most significant disadvantage of using getHtmlClass() as the only CSS selector is that it may not be specific enough to change theme CSS.

Building CSS selectors

ID selectors

The simplest selector is an ID selector as it will always be more specific than any selector in the WComponents theme. Even here it may sometimes be necessary to set some rules as !important in order to override the inline styles we sometimes have to write in order to support structural layout abstractions such as column widths, hgap and vgap.

The main problem with ID selectors is that a WComponent's ID is not necessarily fixed. Even if a static ID is set using setId(String id) it cannot be guaranteed that the CSS selector #SOME_ID will work as IDs are modified if the component is nested within a naming context. There are two ways around this issue. One, which is guaranteed to always work but which is less efficient is to write the CSS inline and use getId() during preparePaintComponent to build the selector. This is illustrated above.

Trying to determine ID selectors from static component IDs

An alternative which allows for cacheable style sheets depends on the full structure of all uses of a component being known. This is extremely fragile and is not recommended.

If this structure is known and the component is either never in a naming context (other than the default WApplication) or is always in a known nesting of naming context(s) where the context ID prefixes are known then the ID will not change.

Given a WButton, for example, where the button has been given the ID "fancySubmitButton" we have two possible fixed ID selectors:

  1. if the button is never in a naming context then the selector will be #fancySubmitButton;
  2. if the button is always in a known naming content "someContext" and a nested naming context "someSubContext" then the selector will be #someContext-someSubContext-fancySubmitButton.

I told you it was fragile!

Class selectors

Class selectors are much easier to deal with than ID selectors but are much less specific. One may have to use some cunning to get a selector specific enough to override theme determined styles. Knowledge of the CSS selectors used in the default WComponents theme may (should) help but these are not strictly enforceable in any application theme as the rendering is done in XSLT which is itself part of the theme and may be overridden.

Inbuilt classes

There are a few groups of inbuilt classes.

  1. The local name of the rendered XML element of any component which extends WComponent. This is as close to a public API of class names as we are willing to get and it is strongly recommended that every WComponents theme supports this. The class name is attached to the outermost (root) element of the HTMl output stream from teh transformation. This includes, for example, the panel local name of WPanel or the collapsible local name of WCollapsible; for this reason some of our class names are in camelCase. All of these are part of components which support setHtmlClass().
  2. The local name of sub-components not necessarily addressable in the Java API but in the schema. This includes such constructs as the ui:flowLayout (etc) child of WPanel's ui:panel, the ui:tbody and ui:tr descendants of WTable. These are not available to attach custom class names using setHtmlClass(String htmlClass) but are descendants of components which are. It is reasonably safe to use these classes in your CSS selectors.
  3. A very small number of fake element-name classes which are applied in certain contexts where there is a known XML element with a particular class but then other HTML artefacts which are not part of the XML input stream implement the same job. It is reasonably safe to use these classes in your selectors but their support is not fixed in stone. At the moment this group consists of:
  • row
  1. classes we make up as we go along which constitute a more-or-less 'private' member equivalence. These all start wc_ and should not usually be included in your CSS selectors unless you have no other option.

It must be noted that there is no guarantee your theme will support any of these, even support for the first is recommended and never enforced.

Adding a custom class

WComponents which extend AbstractWComponent have a method setHtmlClass(String htmlClass) to add value(s) to the class attribute of the root output element. This string may include whitespace in which case each 'word' becomes a class. If, for example, the method is called thus: setHtmlClass("foo bar") then the output element will have a class attribute which includes " foo bar" and a CSS selector .foo.barmay be used. If to take the example further the WComponent is a [[WButton]] (which renders asui:buttonin the XML stream) then the CSS selector could be.button.foo.bar`

The method getHtmlClass() returns the value as set. It does not return a CSS selector. The possibility of adding a method to access CSS selectors for components is under consideration and your views are welcome.

It is possible to add a bunch of "words" to the class attribute using setHtmlClass giving one the option to create quite specific selectors but if one needs to go to these lengths it may be worthwhile revisiting one's theme.

Attribute selectors

Perusing the theme Sass it is clear that WComponents default theme uses a lot of attribute selectors. Many of these selectors (indeed, most of them) are tied to WAI-ARIA roles, states and properties. These WAI-ARIA based attribute selectors are as safe as any for use in custom styles.

Given a WTab, for example, it is safer to create a CSS selector using the attribute selector [role='tab'] than to rely on the XML element local name tab to build a class selector .tab. Wherever possible WComponents will use WAI-ARIA roles, states and properties as the basis for JavaScript functionality and default styling so if something loses its role of tab it will stop behaving like a tab as well as no longer looking like a tab.

Related topics

Clone this wiki locally