Creating a Custom UI for openHAB 2

Intro

OpenHAB is a home automation framework running on top of well known Java technologies like OSGi.

OpenHAB is developed in such a way that the average user could simply open up the prepackaged user interfaces and be able to start communicating with the devices in their home through available bindings. However, it’s often the case that openHAB is being used to develop an automation system for which configurations, settings, and other user interactions are being handled automatically, such as an internally developed application connecting devices in vehicles. In such a case, the default UIs are not appropriate for user interaction.

This article goes through the process of creating a custom UI which can be extended with all the desired behavior, and which does not include the undesirable behavior of the default UIs. Because openHAB is not well documented, this information can be very challenging to find anywhere else.

Creating the UI

Your UI file structure should be as follows. Everything should be under the org.eclipse.smarthome.ui.new package.

File Structure for openHAB UI
File Structure for openHAB UI

NewUIApp.java

NewUIApp.java has to take the httpService from OSGi and register its resources at a given location. This is done with the following code.

package org.eclipse.smarthome.ui.new.internal;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class NewUIApp {

    public static final String WEBAPP_ALIAS = "/newui"; // the root dir of our new ui
    private final Logger logger = LoggerFactory.getLogger(NewUIApp.class);

    protected HttpService httpService;

    protected void activate(ComponentContext componentContext) {
        try {
            // register our resources in the web directory
            httpService.registerResources(WEBAPP_ALIAS, "web", null);
            logger.info("Started New UI at " + WEBAPP_ALIAS);
        } catch (NamespaceException e) {
            logger.error("Error during servlet startup", e);
        }
    }

    protected void deactivate(ComponentContext componentContext) {
        httpService.unregister(WEBAPP_ALIAS);
        logger.info("Stopped New UI");
    }

    protected void setHttpService(HttpService httpService) {
        this.httpService = httpService;
    }

    protected void unsetHttpService(HttpService httpService) {
        this.httpService = null;
    }

}

MANIFEST.MF

The manifest file is used by OSGi to determine the name of the bundle and the bundle package requirements. That should look as follows.

Manifest-Version: 1.0
Bundle-Name: New UI
Bundle-Vendor: ExaminingEverything
Bundle-Version: 0.9.0.qualifier
Bundle-ManifestVersion: 2
Bundle-License: http://www.eclipse.org/legal/epl-v10.html
Import-Package: org.osgi.service.component,
 org.osgi.service.http,
 org.slf4j
Bundle-SymbolicName: org.eclipse.smarthome.ui.new;singleton:=true
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Service-Component: OSGI-INF/*.xml
Bundle-ClassPath: .

You’ll see how the bundle name comes into play later.

newuiapp.xml

This file tells OSGi what services our bundle provides. In this case, it simply provides the UI app Java class that we created, and it also includes a reference tag to get the HttpService injected at runtime.

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="activate" deactivate="deactivate" name="org.eclipse.smarthome.ui.new.internal.NewUIApp">
   <implementation class="org.eclipse.smarthome.ui.new.internal.NewUIApp"/>
   <reference bind="setHttpService" cardinality="1..1" interface="org.osgi.service.http.HttpService" name="HttpService" policy="static" unbind="unsetHttpService"/>
</scr:component>

Cardinality 1..1 refers to the fact that our request is satisfied by exactly one HttpService instance.

index.html

Index.html can contain whatever you like. This is there simply to show that the new UI is registered in openHAB. The next step for the UI, which this article does not cover, would be tying together the UI elements with API calls to the openHAB instance. You can look at the existing Paper UI implementation for an example of this.

Here’s a simple index.html page.

<!DOCTYPE HTML>
<html>
  <head>
    <title>New UI</title>
  </head>
  <body>
    <h1>NEW UI TEST!</h1>
  </body>
</html>

pom.xml

The pom.xml is used for building in Maven. This file will have to have all the necessary information to build the project as an eclipse plugin, and to create the correct output for an OSGi bundle so we can register it with openHAB later.

The POM should look like this. The reason for the giant POM is that all the parent POMs from the UI project POM all the way up to the smarthome library have to have their configurations included here for the build to work. Normally they would be included by their location relative to the UI POM, but we wont always want our POM located in a specific place.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <modelVersion>4.0.0</modelVersion>


  <artifactId>org.eclipse.smarthome.ui.new</artifactId>
  <groupId>org.eclipse.smarthome.ui</groupId>
  <version>0.9.0-SNAPSHOT</version>

  <name>New UI</name>
  <packaging>eclipse-plugin</packaging>

  <properties>
    <esh.java.version>1.8</esh.java.version>
    <maven.compiler.source>${esh.java.version}</maven.compiler.source>
    <maven.compiler.target>${esh.java.version}</maven.compiler.target>
    <maven.compiler.compilerVersion>${esh.java.version}</maven.compiler.compilerVersion>
    <tycho-version>1.0.0</tycho-version>
    <tycho-groupid>org.eclipse.tycho</tycho-groupid>
    <xtext-version>2.12.0</xtext-version>
    <karaf.version>4.0.3</karaf.version>
    <ds-annotations.version>1.2.8</ds-annotations.version>
    <jdt-annotations.version>2.1.0</jdt-annotations.version>
    <build.helper.maven.plugin.version>1.8</build.helper.maven.plugin.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

<build>
    <plugins>
      <plugin>
        <groupId>org.eclipse.tycho</groupId>
        <artifactId>tycho-source-plugin</artifactId>
        <version>${tycho-version}</version>
        <executions>
          <execution>
            <id>plugin-source</id>
            <goals>
              <goal>plugin-source</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.felix</groupId>
        <artifactId>maven-scr-plugin</artifactId>
        <executions>
          <execution>
            <id>generate-scr-scrdescriptor</id>
            <goals>
              <goal>scr</goal>
            </goals>
          </execution>
        </executions>
      </plugin>   
<plugin>
        <groupId>${tycho-groupid}</groupId>
        <artifactId>tycho-maven-plugin</artifactId>
        <version>${tycho-version}</version>
        <extensions>true</extensions>
      </plugin>
      <plugin>
        <groupId>${tycho-groupid}</groupId>
        <artifactId>target-platform-configuration</artifactId>
        <configuration>
          <environments>
            <environment>
              <os>linux</os>
              <ws>gtk</ws>
              <arch>x86</arch>
            </environment>
            <environment>
              <os>linux</os>
              <ws>gtk</ws>
              <arch>x86_64</arch>
            </environment>
            <environment>
              <os>win32</os>
              <ws>win32</ws>
              <arch>x86</arch>
            </environment>
            <environment>
              <os>win32</os>
              <ws>win32</ws>
              <arch>x86_64</arch>
            </environment>
            <environment>
              <os>macosx</os>
              <ws>cocoa</ws>
              <arch>x86_64</arch>
            </environment>
          </environments>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-clean-plugin</artifactId>
        <version>2.5</version>
        <configuration>
          <filesets>
            <fileset>
              <directory>${basedir}/xtend-gen</directory>
              <includes>
                <include>**</include>
              </includes>
              <excludes>
                <exclude>.gitignore</exclude>
              </excludes>
            </fileset>
            <fileset>
              <directory>${basedir}/src/main/generated-sources/xtend</directory>
              <includes>
                <include>**</include>
              </includes>
              <excludes>
                <exclude>.gitignore</exclude>
              </excludes>
            </fileset>
          </filesets>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
      </plugin>   
    </plugins>
<pluginManagement>
      <plugins>
        <plugin>
          <groupId>${tycho-groupid}</groupId>
          <artifactId>tycho-compiler-plugin</artifactId>
          <version>${tycho-version}</version>
          <configuration>
            <extraClasspathElements>
              <extraClasspathElement>
                <groupId>org.apache.felix</groupId>
                <artifactId>org.apache.felix.scr.ds-annotations</artifactId>
                <version>${ds-annotations.version}</version>
              </extraClasspathElement>
              <extraClasspathElement>
                <groupId>org.eclipse.jdt</groupId>
                <artifactId>org.eclipse.jdt.annotation</artifactId>
                <version>${jdt-annotations.version}</version>
              </extraClasspathElement>
            </extraClasspathElements>
            <compilerArgs>
              <arg>-err:+nullAnnot(org.eclipse.jdt.annotation.Nullable|org.eclipse.jdt.annotation.NonNull|org.eclipse.jdt.annotation.NonNullByDefault),+inheritNullAnnot</arg>
              <arg>-warn:+null,+inheritNullAnnot,+nullAnnotConflict,+nullUncheckedConversion,+nullAnnotRedundant,+nullDereference</arg>
            </compilerArgs>
          </configuration>          
        </plugin>
        <plugin>
          <groupId>${tycho-groupid}</groupId>
          <artifactId>target-platform-configuration</artifactId>
          <version>${tycho-version}</version>
          <configuration>
            <!--
            <resolver>p2</resolver>
            <ignoreTychoRepositories>true</ignoreTychoRepositories>
            -->
            <pomDependencies>consider</pomDependencies>
            <target>
              <artifact>
                <groupId>org.eclipse.smarthome</groupId>
                <artifactId>targetplatform</artifactId>
                <version>${project.version}</version>
                <classifier>smarthome</classifier>
              </artifact>
            </target>
          </configuration>
        </plugin>
        <plugin>
          <groupId>${tycho-groupid}</groupId>
          <artifactId>tycho-surefire-plugin</artifactId>
          <version>${tycho-version}</version>
          <configuration>
            <failIfNoTests>false</failIfNoTests>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.apache.felix</groupId>
          <artifactId>maven-scr-plugin</artifactId>
          <version>1.24.0</version>
          <configuration>
            <supportedProjectTypes>
              <supportedProjectType>eclipse-plugin</supportedProjectType>
            </supportedProjectTypes>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>build-helper-maven-plugin</artifactId>
          <version>${build.helper.maven.plugin.version}</version>
          <executions>
            <execution>
              <id>add-source</id>
              <phase>generate-sources</phase>
              <goals>
                <goal>add-source</goal>
              </goals>
              <configuration>
                <sources>
                  <source>src/main/groovy</source>
                </sources>
              </configuration>
            </execution>
            <execution>
              <id>add-test-source</id>
              <phase>generate-test-sources</phase>
              <goals>
                <goal>add-test-source</goal>
              </goals>
              <configuration>
                <sources>
                  <source>src/test/groovy</source>
                </sources>
              </configuration>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>org.apache.felix</groupId>
          <artifactId>maven-bundle-plugin</artifactId>
          <version>3.0.1</version>
          <extensions>true</extensions>
          <configuration>
            <supportedProjectTypes>
              <supportedProjectType>jar</supportedProjectType>
              <supportedProjectType>bundle</supportedProjectType>
              <supportedProjectType>eclipse-plugin</supportedProjectType>
            </supportedProjectTypes>
          </configuration>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.6.1</version>
          <configuration>
            <compilerId>groovy-eclipse-compiler</compilerId>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>compile</goal>
              </goals>
            </execution>
          </executions>
          <dependencies>
            <dependency>
              <groupId>org.codehaus.groovy</groupId>
              <artifactId>groovy-eclipse-compiler</artifactId>
              <version>2.9.2-01</version>
            </dependency>
            <dependency>
              <groupId>org.codehaus.groovy</groupId>
              <artifactId>groovy-eclipse-batch</artifactId>
              <version>2.4.3-01</version>
            </dependency>
          </dependencies>
        </plugin>
        <plugin>
          <groupId>com.mycila</groupId>
          <artifactId>license-maven-plugin</artifactId>
          <version>3.0</version>
          <configuration>
            <basedir>${basedir}</basedir>
            <header>src/etc/header.txt</header>
            <quiet>false</quiet>
            <failIfMissing>true</failIfMissing>
            <strictCheck>true</strictCheck>
            <aggregate>true</aggregate>
            <useDefaultMapping>true</useDefaultMapping>
            <mapping>
              <xtend>JAVADOC_STYLE</xtend>
              <mwe2>JAVADOC_STYLE</mwe2>
            </mapping>
            <includes>
              <include>src/**/*.java</include>
              <include>src/**/*.groovy</include>
              <include>src/**/*.xtend</include>
              <include>src/**/*.mwe2</include>
              <include>bin/**/*.mwe2</include>
              <include>workflows/**/*.mwe2</include>
              <include>src/main/feature/feature.xml</include>
              <include>feature.xml</include>
              <include>OSGI-INF/*.xml</include>
            </includes>
            <excludes>
              <exclude>_*.java</exclude>
            </excludes>
            <useDefaultExcludes>true</useDefaultExcludes>
            <properties>
              <year>2017</year>
            </properties>
            <encoding>UTF-8</encoding>
          </configuration>
          <executions>
            <execution>
              <goals>
                <goal>check</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>org.eclipse.xtend</groupId>
          <artifactId>xtend-maven-plugin</artifactId>
          <version>${xtext-version}</version>
          <executions>
            <execution>
              <goals>
                <goal>compile</goal>
                <goal>xtend-install-debug-info</goal>
                <goal>testCompile</goal>
                <goal>xtend-test-install-debug-info</goal>
              </goals>
              <configuration>
                <outputDirectory>${basedir}/xtend-gen</outputDirectory>
                <testOutputDirectory>${basedir}/xtend-gen</testOutputDirectory>
              </configuration>
            </execution>
          </executions>
        </plugin>
        <plugin>
          <groupId>${tycho-groupid}</groupId>
          <artifactId>tycho-versions-plugin</artifactId>
          <version>${tycho-version}</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-clean-plugin</artifactId>
          <version>2.5</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

  <dependencies>
    <dependency>
      <groupId>org.apache.felix</groupId>
      <artifactId>org.apache.felix.scr.ds-annotations</artifactId>
      <version>${ds-annotations.version}</version>
      <optional>true</optional>
    </dependency>
  </dependencies> 

<profiles>
    <profile>
      <id>sign</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.eclipse.cbi.maven.plugins</groupId>
            <artifactId>eclipse-jarsigner-plugin</artifactId>
            <version>1.0.5</version>
            <executions>
              <execution>
                <id>sign</id>
                <phase>verify</phase>
                <goals>
                  <goal>sign</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
    <profile>
      <id>QA</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.7.4.201502262128</version>
            <configuration>
              <dataFile>${session.executionRootDirectory}/target/coverage.jacoco</dataFile>
              <destFile>${session.executionRootDirectory}/target/coverage.jacoco</destFile>
              <append>true</append>
              <excludes>
                <exclude>**/*Test.*</exclude>
              </excludes>
            </configuration>
            <executions>
              <execution>
                <id>default-prepare-agent</id>
                <goals>
                  <goal>prepare-agent</goal>
                </goals>
              </execution>
              <execution>
                <id>default-prepare-agent-integration</id>
                <goals>
                  <goal>prepare-agent-integration</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
    <!-- We need this profile in order to set '-Xdoclint:none' as a project property which will be used later by maven-javadoc-plugin as an 'additionalparam' to be passed to the javadoc.exe. -->
    <!-- This option will be used only if the JDK version is 1.8 or higher. Earlier versions of javadoc.exe does not accept this option. -->
    <profile>
      <id>doclint-java8-disable</id>
      <activation>
        <jdk>[1.8,)</jdk>
      </activation>
      <properties>
        <javadoc.opts>-Xdoclint:none</javadoc.opts>
      </properties>
    </profile>
    <profile>
      <id>javadoc</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
            <version>2.10.3</version>
            <executions>
              <execution>
                <id>aggregate</id>
                <goals>
                  <goal>aggregate-jar</goal>
                </goals>
              </execution>
            </executions>
            <configuration>
              <!-- 'javadoc.opts' project property is set by the 'doclint-java8-disable' profile. It is important to keep 'javadoc' profile declaration after the declaration of 'doclint-java8-disable' profile. -->
              <additionalparam>${javadoc.opts}</additionalparam>
              <excludePackageNames>*.internal.*,nl.*</excludePackageNames>
            </configuration>
          </plugin>
          <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>build-helper-maven-plugin</artifactId>
            <version>${build.helper.maven.plugin.version}</version>
            <executions>
              <execution>
                <id>attach-artifacts</id>
                <phase>install</phase>
                <goals>
                  <goal>attach-artifact</goal>
                </goals>
                <configuration>
                  <artifacts>
                    <artifact>
                      <file>${project.build.outputDirectory}/${project.artifactId}-${project.version}.jar</file>
                      <type>jar</type>
                      <classifier>javadoc</classifier>
                    </artifact>
                  </artifacts>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles> 

</project>

build.properties

This file contains the build info for your jar output. Specifically, it tells Maven what items to include in the build and where to output the class files.


output.. = target/classes/
bin.includes = META-INF/,\
               .,\
               OSGI-INF/,\
               web/index.html
source.. = src/main/java/

Adding the bundle to OpenHAB

Package UI

First, you need to run the mvn package command in your base directory to create a package that you can register as a bundle with OSGi. On Windows, you may need to download Maven and extract it, then add it to your PATH. For Linux systems, you can run apt-get install maven. Your output should look like this:

openHAB UI Build Output
openHAB UI Build Output

Start openHAB

Installing openHAB is relatively easy on both Linux distros and Windows. Just unzip the files provided by openHAB, and run the start script from your shell. For Linux-based systems you may also have to ssh into the openHAB instance by running ssh openhab@localhost. The default password is habopen.

Install the bundle

The bundle:install command expects a URL, so you will have to type your package file path like so: file://localhost/[root_dir]/org.eclipse.smarthome.ui.new/target/org.eclipse.smarthome.ui.new-SNAPSHOT-[version].jar. Essentially, you provide the full path to the package jar as a file URL to register your new UI bundle.

Install OpenHAB Bundle
Install OpenHAB Bundle

Next, you should call bundle:start "New UI" to start the bundle.

Start openHAB UI Bundle
Start openHAB UI Bundle

View the page

Because we did not also register a dashboard tile bundle for the front-end, you must navigate directly to the new UI. You can do this by navigating to http://localhost:8080/newui/index.html in your browser. The HttpService that we had injected into our UI by OSGi will then serve up the index page from the location that we registered (web directory).

View openHAB UI
View openHAB UI

Updating the bundle

From now on, when you build your package, you only need to call bundle:update "New UI" and it will fetch it from the previous location. You will need to install the new bundle if the version changes, however.

Conclusion

OpenHAB is a great home automation system, but is designed more to be user-friendly rather than developer-friendly. There are many small tips and tricks like this needed to turn openHAB into a commercial production automation system. Creating a UI for openHAB is very simple – once you understand OSGi and the openHAB framework. This guide should provide you a starting point to develop your own system on top of openHAB.

Notes

Dashboard tiles

If you want your UI to show up in the list of tiles when the application opens, you’ll also have to extend the org.openhab.ui.dashboard.DashboardTile class, and provide your implementation of it to OSGi as a bundle. You can see how this is done by looking at the existing implementation for Paper UI.

API calls

The actual API calls needed to create an openHAB UI are varied and too many to go over here. The default openHAB installation provides a UI which allows you to test the various APIs, so you can use this as a starting point. You can also look at Paper UI if you want to see an implementation of the API calls based on AngularJS v1.

CSS Stacking Contexts

Intro

Today we’ll be learning about a lesser-known feature of CSS: Stacking contexts.

You may have been working on a project before and been surprised when you set the z-index for an element and it refused to move forward, remaining behind some other stubborn element.

There’s a reason for this behavior, and that reason is stacking contexts.

Stacking Context

A stacking context is essentially a group of elements whose z-index value determines their position within that group. If two elements do not share a stacking context, then they will ignore each other’s z-index values.

In this case, the stacking order is based on their relative order in the DOM (See image under “Creating a Stack”).

Creating a Stack

All of the common stacking context types.
All of the common stacking context types. Order is relative, fixed, absolute, opacity, transform.

A stacking context is created in the following cases:

  • The root stacking context (html element)
  • Absolute or relative position with a set z-index
  • Fixed position
  • Opacity less than 1
  • A set transform
  • A few other less common instances

I’ll be covering only the common instances that developers will normally encounter.

The Root Stacking Context

This case is pretty clear. Initially, all elements are part of a single stacking context under the DOM, meaning that their relative position on the z axis is determined entirely by their z-index property. If no z-index is set, their order is determine by the order in which they appear in the DOM (See image under “Creating a Stack”).

Absolute or Relative Position With a Set Z-Index

This case is the second-most common. This is almost always intentional, but occasionally, developers may try to position an element in another stacking context over some absolutely positioned element and find that it’s not possible.

Fixed Position

Another common case, but one that can be confusing. Most but not all browsers have this behavior now. Fixed position elements create their own stacking context, which without a z-index normally places it behind the document root. This can create a case of disappearing elements.

Opacity less than 1

This is a rare case, but one that everyone should be aware of. If you’re going to set opacity, then you have to know the consequence will be a new stacking context. If all you want is a translucent element, it will be more predictable if you simple set an rgba background with an alpha less than 1.

The reason for this is clear: If it did not create a new stacking context, what elements would show through the transparent element?

A Set Transform

This is a case which is more and more common lately, as CSS transforms become the norm. This often throws people off, as we assume when we scale an element it should retain its position in the flow of the document. The new stacking context can cause a transformed element to hide menus and other elements which would normally appear in front.

How Stacking Contexts Interact

Of course, the most important thing is how to apply this knowledge to create layouts and fix problems in the real world. For this reason, I’ve supplied some examples of how stacking contexts interact with each other. Most importantly, how do their children determine their z-positioning relative to other stacking context’s children?

Well, using the example from “Creating a Stack”, here’s what happens:

Z-Index Set

Z-Index on Relative Element's Children
Z-Index on Relative Element’s Children

If we set the z-index of the child elements, the result is the same as our original elements.

Z-Index Positive, Position Relative

Z-Index on Relative Element's Children - Children Are Relatively Positioned
Z-Index on Relative Element’s Children – Children Are Relatively Positioned

If we set the z-index of the child elements to a positive value, but additionally set the children’s positions to relative (creating a new stacking context for each child), then they will position completely independently of their parent, moving out in front of the other elements.

Z-Index Negative, Position Relative

Negative Z-Index on Relative Element's Children - Children Are Relatively Positioned
Negative Z-Index on Relative Element’s Children – Children Are Relatively Positioned

If we set the z-index of the child elements to a negative value, but additionally set the position to relative (creating a new stacking context for each child), then they will position completely independently of their parent, moving behind the other elements.

Z-Index Greater Than Other Stacking Context’s Children

Relative and Fixed Element with a Set Z-Index, Children With Set Z-Index Values
Relative and Fixed Element with a Set Z-Index, Children With Set Z-Index Values

In this case, we have given both the relative element, and the fixed element a z-index. The z-indices of their children do not interact, so even though the relative children are positioned ahead of the fixed children, they do not appear that way. The children are each in separate stacking contexts, though their parents share the same stacking context.

Conclusion

Stacking contexts are groups of elements whose z-index values position them along the y axis relative to each other. If an element is the root of a stacking context, its children will ignore the z-index values of the children of other stacking contexts, even if they are larger than its own.

Stacking contexts are very important when creating layouts in CSS. A lack of understanding of stacking contexts can lead to difficulty implementing relatively simple UIs, and in fixing bugs which arise commonly in today’s UIs. Stacking contexts are very commonly created when showing things like menus, popups, windows, etc. These types of UI controls are very common in web applications today, and therefore so is knowledge of stacking contexts and how they interact.