CSS-Selektoren – „Kurz mal was testen“

Ob es nur das Anpassen einer Schriftgröße oder das komplexe Positionieren von ganzen Content-Bereichen ist: Kein Weg führt im Webdesign an Cascading Style Sheets (CSS) vorbei. Was auch immer man von ihnen hält, sie sind unumgänglich, wenn man dem Layout einer Website mehr abverlangen will, als den nackten Browserstandard. Wenn man sich mit CSS beschäftigt, dann oft mit den einzelnen Stilattributen. Margin, padding, border, float und viele mehr werden in CSS genutzt, um das Aussehen und die Positionierung von Elementen zu beschreiben und sind oft ein Grund für Kopfzerbrechen. Damit soll sich dieser Artikel jedoch nicht beschäftigen. Hier soll vielmehr ein Blick hinter die Kulissen geworfen werden: Wie wertet der Browser unsere Stilangaben aus, wie werden sie auf Elemente angewandt und welche Möglichkeiten gibt es überhaupt?

css-is-awesome

Fast alle der Beispiele in diesem Block sind in zwei Spalten aufgeteilt. Die linke Spalte zeigt immer eine Vorschau des Beispiels und die rechte Spalte den dazugehörigen Code. Ein Klick auf die Code-Ansicht führt zu einem Textfeld, in dem der Code direkt editiert werden kann. Sämtliche Änderungen hier werden automatisch auf die Vorschau übertragen, um die Anpassungen direkt in Aktion zu sehen. Um die Eingaben rückgängig zu machen, klicken Sie einfach auf das kleine Reload-Icon in der rechten oberen Ecke.

Selektoren

Der einfachste Weg ein Element anzupassen, führt über das style-Attribut, das fast jedes HTML-Element unterstützt. Stilinformationen werden direkt mit dem Inhalt der Seite verknüpft und vom Browser ausgewertet:

Diese Formatierungsart ist im Normalfall der “worst-case”, sprich die problematischste Art der Umsetzung. Elemente, die gleich aussehen sollten, müssten immer alle Stilinformationen tragen. Die Wartbarkeit ist nicht gegeben und der Inhalt ist in keiner Weise vom Aussehen getrennt, was zum Beispiel Themeing nahezu unmöglich macht. Besser wäre es, die Stilinformation von dem HTML-Code zu trennen, um so Regeln für bestimmte Elemente in einer separaten CSS-Datei zu formulieren. Dafür müsste es jedoch möglich sein, bestimmte Elemente auszuwählen, die mit einer Regel angepasst werden sollen. Genau zu diesem Zweck existieren in CSS die sogenannten Selektoren.

Der einfachste Selektor ist die simple Auswahl eines HTML-Tags, wie zum Beispiel einem Link (<a …>…</a>). In dem folgenden Beispiel wird die Schriftfarbe für alle Links auf Rot gesetzt. a ist hier der Selektor, der gesamte Block wird als Regel bezeichnet:

Da es eher ungewöhnlich ist, dass eine Regel auf alle Elemente eines Typs angewandt werden soll, gibt es die häufiger benutzten Id- (#) und Klassen-Selektoren (.):

Diese Selektoren können auf gleicher Ebene kombiniert werden. So würde li.menu-link nur alle <li>-Tags mit der Klasse .menu-link treffen, alle anderen <li>-Tags werden ignoriert. div#header würde das <div> auswählen, das die ID header hat (also <div id='header'>..</div>). Das lässt sich fast beliebig fortführen, wie zum Beispiel a#my-link.highlighted. Wichtig ist nur, dass kein Leerzeichen zwischen den einzelnen Teilen steht. Warum das so ist, wird im nächsten Abschnitt verdeutlicht.

Hierarchien

Die einzelnen oder kombinierten Selektoren sind schon ganz nett, viel Freiheit ermöglichen sie leider noch nicht. Will man zum Beispiel allen <li>-Tags in einer Liste einen bestimmten Stil geben, dann wäre es schön, einen Selektor von der Art „Alle <li>-Tags in einer Liste mit der Klasse menu-list“ zu formulieren. Glücklicherweise bietet CSS genau zu diesem Zweck hierarchische Selektoren. In dem folgenden Beispiel beschreiben A und B immer einfache Selektoren – wie im obigen Abschnitt beschrieben – die zu einem kombinierten Selektor zusammengefügt werden.

A, B – Das OR in CSS

Das Komma ist streng genommen kein hierarchischer Selektor, sondern fungiert als Oder: Wende dieses Aussehen auf alle Elemente A oder B (oder C, oder D, …) an. Oft findet man diese Verknüpfung, wenn eine CSS-Klasse die Bedeutung eines Elements beschreibt, je nach Elementtyp aber ein anderes Aussehen vergeben werden soll. Das ist zum Beispiel in UI-Frameworks der Fall, wo es eine Klasse ui-widget gibt, die Komponenten auf der UI bezeichnen. Input- und Button-Tags haben dabei gleiche Stilinformationen, selectbox-Tags dagegen andere:

button.ui-widget, input.ui-widget {
    padding: 0 5px;
}
selectbox.ui-widget {
    padding 0 10px;
}

Die Angaben könnten auch gleichwertig in zwei getrennten Regeln mit duplizierten Stilangaben gemacht werden, es handelt sich also nur um eine Hilfe für den Entwickler.

A B – Der Familienstamm

Dieser Selektor ist wohl einer der am häufigsten benutzten. Die gegebenen Stilinformationen werden auf alle Elemente B angewandt, die ein Nachfahre – also Kinder, Kindes-Kinder und so weiter – von A sind. Die Stilinformationen werden dabei in der Regel auf das letzte Glied in der Kette angewandt, die vorherigen Selektoren dienen nur der Einschränkung.

A > B – die Sprösslinge

Ähnlich wie in dem kombinierten Selektor A B bezieht sich das > auf Kinder. Im Gegensatz zu vorher werden allerdings nur direkte Kinder angesprochen. Würde man das obige Beispiel .menu a also als .menu > a schreiben, dann wird die Schriftgröße nicht angepasst, da das a-Tag ein Kind von li und nicht von ul.menu ist. Für das gleiche Resultat kann der Selektor wie folgt angepasst werden:

A ~ B und A+B – Brüder und Schwestern

Die beiden kombinierten Selektoren ~ und + sind nicht so häufig anzutreffen, da ihr primäres Einsatzgebiet heute viel lesbarer über neue Pseudoklassen abgebildet werden kann. Nichts desto trotz sollen sie hier kurz erklärt werden. Mit dem Selektor A + B werden alle Elemente B ausgewählt, die unmittelbar auf ein A folgen. A ~ B dagegen würden alle Elemente B selektieren, die irgendwann auf ein A folgen. Beides bezieht sich dabei jedoch nur auf die gleiche Hierarchieebene, Eltern- oder Kind-Knoten werden nicht berücksichtigt. Das klingt recht unübersichtlich, wird aber anhand des folgenden Beispiels etwas klarer:

Attribute und Pseudoklassen

Da HTML ein XML-Dialekt ist, gibt es natürlich an fast allen HTML-Tags Attribute. Entsprechend bietet auch CSS Mittel und Wege, diese als Selektoren zu nutzen. So würde zum Beispiel div[id^="content_"] alle DIV-Tags, deren ID-Attribut mit content_ beginnt, ansprechen (<div id="content_footer"> , <div id="content_sidebar">).

Daneben gibt es sogenannte Pseudoklassen. Diese beschreiben Selektoren, die sich nicht auf Klassen oder IDs beziehen, sondern mehr auf indirekte Eigenschaften eines Elements. Der Selektor a:hover beispielsweise trifft genau dann auf ein A-Tag zu, wenn die Maus im Browser über diesem schwebt. Diese Selektorgruppe ist seit der ersten Version von CSS stetig gewachsen. Hat es mit einem simplen :hover oder :focus angefangen, so gibt es in CSS 3 mittlerweile eine ganze Palette an Selektoren, die gängige Probleme elegant lösen können.

Die folgenden Beispiele stellen eine kleine Auswahl dar, eine vollständige Liste der Attribut- und Pseudoselektoren ist auf der Seite der W3Schools zu finden.

:first-child / :first-of-type und :last-child / :last-of-type

Diese Pseudoklassen machen exakt das, was man anhand des Namens von ihnen erwarten würde: Sie treffen auf das erste oder letzte Kind in einem Knoten zu.

Die -of-type-Versionen müssen immer an einem normalen Selektor stehen und unterscheiden dann zusätzlich nach dem Elementtyp.

:nth-child(n)

(Pseudo-)Code wie den folgenden hat vermutlich schon jeder gesehen, der schon mal versucht hat, eine tabellarische Ansicht von Daten aufzubereiten:

<table>
    <tr class="iter.index%2 == 0 ? even : odd">
        ...
    </tr>
</table>

Die geraden und ungeraden Zeilen kriegen jeweils eine Klasse, in der dann zum Beispiel die Hintergrundfarbe leicht angepasst wird. CSS 3 bietet mit dem nth-child(n)-Selektor eine neue Möglichkeit, die auf solche Hilfsmittel verzichtet. In der einfachen Fassung erwartet der Selektor eine ganze Zahl, die angibt, das wievielte Kind innerhalb des Elternknotens selektiert werden soll. Zusätzlich bietet er aber auch die Möglichkeit, eine einfache lineare Gleichung a*n + b anzugeben. n startet dabei bei 0, 3n+1 würde also die Kinder 1, 4, 7, 10, ... selektieren und 2n entsprechend 2, 4, 6, ... (das Kind 0 gibt es hier nicht).

:not

Im Gegensatz zu den vorherigen Selektoren, die geprüft haben, ob etwas zutrifft, kann mit :not(A) das Nichtzutreffen der Bedingung A formuliert werden. A muss dabei ein einfacher Selektor sein und bezieht sich genau auf das Element, das mit dem vorgestellten Selektor ausgewählt wurde. So lässt sich zum Beispiel prüfen, ob eine bestimmte Klasse nicht gesetzt wurde, ein Attribut einen bestimmten Text nicht enthält oder ein Element einen bestimmten Typ nicht hat:

Und alles auf einmal – kombinieren von Selektoren

Die oben genannten Selektoren lassen sich fast beliebig kombinieren, um nur genau die Elemente zu selektieren, auf die ein Stil angewandt werden soll. Werden die Ketten zu lang, dann sollte man sich allerdings überlegen, ob eine neue Klasse an der richtigen Stelle nicht doch ein besseres Ergebnis liefert.

.header .menu:hover h1:not(.disabled) {
    ...
}

.block[id^="section_"] .title > h1 {
    ...
}

Kombinierende Kaskadierung

Bevor es zu dem letzten Punkt dieses Artikels geht, fehlt noch eine kleine, aber wichtige Eigenschaft von CSS, die sich bereits im Namen versteckt: Die Kaskadierung (Cascading). Diese macht sich auf zwei Arten bemerkbar. Zum einen werden bestimmte Stilinformationen von dem Elternknoten auf alle seine Kindknoten vererbt, wenn sie für den Kindknoten nicht explizit gesetzt werden. Damit muss zum Beispiel die Schriftgröße nicht immer wieder gesetzt werden, sondern es reicht, sie an einem ausgewählten Elternknoten zu definieren, wie der folgende Ausschnitt zeigt. Ob ein Stilattribut vererbt wird, lässt sich für die Schriftgröße zum Beispiel auf der Seite des Mozilla Developer Networks unter dem Punkt Inherited nachschauen.

Zum anderen ist CSS in der Lage, Stilinformationen zu kombinieren, wenn auf ein Element zwei oder mehr Regeln zutreffen. So würde das folgende Beispiel das Span-Element nicht nur in doppelter Schriftgröße, sondern zusätzlich rot darstellen. Beide Regeln treffen auf das Element zu. Der Browser nutzt also die font-size-Anweisung aus der ersten und color aus der zweiten Anweisung. Kommentiert man die zweite font-size-Anweisung ein, dann würde sie die Angabe aus dem span-Selektor überschreiben:

Durch das Zusammenspiel der beiden Eigenschaften ergibt sich für jedes Element der Website ein eindeutiger Satz an Informationen, auf deren Basis der Browser sich an das Zeichnen der Elemente machen kann.

Specificity

Wenn man sich das obige Beispiel anschaut, dann kommt unweigerlich eine Frage auf: Woher weiß der Browser, in welcher Reihenfolge die CSS-Regeln angewandt werden, also welche Stilanweisung Vorrang hat? Eine Ausprägung dieser Frage lässt sich am besten mit einem Gedanken festhalten, den vermutlich jeder, der schon einmal mit CSS gearbeitet hat, kennt: “Warum wird jetzt ausgerechnet dieser Style angewandt, ich habe das doch anders definiert?”. Oft ist die ‘Lösung’ ein !important, um seine eigene Stilangabe bevorzugt zu behandeln. Nimmt man in dem folgenden Ausschnitt das !important raus, dann werden alle Texte rot angezeigt:

Gerade das führt aber auf lange Sicht zu wesentlich mehr Problemen, da es immer schwieriger wird, die Kaskadierung von CSS sauber zu nutzen, ohne sich Umwegen und Hacks zu bedienen.
Stattdessen kann man sich die Specificity von CSS zu Nutze machen. Diese regelt welche Priorität eine Regel bekommt. In der Kaskadierung werden dann Stilangaben mit niedriger Priorität durch Angaben mit höherer überschrieben. Die Priorität berechnet sich nach einem recht einfachen Muster und besteht aus vier Stellen, die jeweils einen Wert von 0 bis (theoretisch) unendlich einnehmen können:

specificity

 

Pro Komponente in einem Ausdruck wird das entsprechende Feld in der Schablone um eins erhöht. Anschließend können zwei Stilangaben von links nach rechts miteinander verglichen werden. So wäre (0, 1, 0, 2) zum Beispiel höher bewertet als (0, 0, 3, 2) und so weiter. Das klingt beim ersten Lesen kompliziert, ist aber eigentlich ganz simpel. Der Selektor li.default hat eine Klasse und ein Element, entsprechend also die Specificity (0, 0, 1, 1). Damit ist sie aber schon höher einzustufen als div > ul > li, das nur auf (0, 0, 0, 3) kommt. Hier noch ein paar weitere Beispiele:

  • #mainmenu a.highlighted ergibt (0, 1, 1, 1)
  • div.content h1.header img ergibt (0, 0, 2, 3)
  • #mainmenu #mainsubsection #mainlink ergibt (0, 3, 0, 0)
  • .ui-widget .ui-input, .ui-widget .ui-select ergibt (0, 0, 2, 0) und (0, 0, 2, 0). In diesem Fall sind die beiden Selektoren eigenständig, werden also auch getrennt berechnet und angewandt!
  • <span style="color: red">...</span> ergibt (1, 0, 0, 0)

Und für das obige Beispiel:

  • .menu > li ergibt (0, 0, 1, 1)
  • .default ergibt (0, 0, 1, 0)
  • .menu .default ergibt (0, 0, 2, 0)

Bei gleicher Specificity wird die Reihenfolge im Code berücksichtigt. Je ‘später’ die Angabe vom Browser gefunden wird, umso höher wird sie gewichtet.

Stilangaben, die mit !important gekennzeichnet sind, werden bei der Kaskadierung in einer eigenen Prioritätsliste gesammelt, die nach allen anderen angewandt werden. Sie überschreiben also auch Angaben in dem style-Attribut! Das hat zur Folge, dass danach ebenfalls wieder mit !important gearbeitet werden muss, um den Stil zu überschreiben. Kurzfristig mag !important eine Lösung sein, in der Regel wird das Problem dadurch aber nur verschoben und vergrößert.

 

Performance

Auch wenn der Performance-Aspekt in den meisten Anwendungen und modernen Browsern keine so große Rolle mehr spielt, so ist es doch interessant, die Hintergründe im Kopf zu haben. Gerade bei Apps für mobile Geräte, deren Browser eventuell nicht ganz so viel Rechenpower zur Verfügung haben, mag es vielleicht doch einen Unterschied geben.
Allgemein spielen bei der Geschwindigkeit von Selektoren zwei Faktoren eine Rolle: Die Reihenfolge des Matching und die Performance eines Matchers.
Erster enthält vermutlich die größere Überraschung: CSS startet die Auswertung eines Selektors mit dem letzten Selektor in der Kette, dem sogenannten Key-Selektor. Von da arbeitet es sich den Baum nach oben, in dem es immer einen Schritt weiter nach links geht und prüft, ob ein Treffer existiert. Das bedeutet aber, dass zum Beispiel für den Selektor #menu li span erst alle span-Elemente gesucht werden, dann für jedes dieser Elemente geprüft wird ob es Nachfahre eines li-Tags ist, das wiederum in einem Element mit der ID menu steht. Man kann sich leicht vorstellen, dass bei diesem Verfahren wesentlich mehr geprüft wird, als wenn nur der Bereich #menu nach allen li-Tags und anschließend nach span-Tags durchsucht wird. Im ersten Moment wirkt dieses Vorgehen umständlich, für den typischen Anwendungsfall ist es aber am besten geeignet. Der Browser trifft beim Rendern einer Website nach und nach auf die Elemente, aus denen die Seiten besteht. Für jedes dieser Elemente muss er, wie im vorherigen Abschnitt beschrieben, die Stilinformationen bestimmen. Startet er mit dem Key-Selektor, dann kann er einen Großteil der definierten Regeln direkt als irrelevant aussortieren. Anschließend muss er nur noch eine Handvoll von Regeln weiter prüfen.

Der zweite Faktor bezieht sich auf die Geschwindigkeit des Key-Selectors, den CSS in vier Kategorien gruppiert:

  • ID-Rules (#content, div#header)
  • Class-Rules (.menu, a.highlighted)
  • Tag-Rules (span, a)
  • Universal-Rules (:first-child, [title*="Lorem"], *).

Die Kategorien sind dabei in der Reihenfolge ihrer Geschwindigkeit angeordnet, ein ID-Selektor ist zum Beispiel schneller als ein Class-Selektor. Das liegt aber nicht daran, dass die eigentliche Prüfung schneller ist, sondern ganz simpel an der Anzahl zu prüfender Elemente. Jede ID sollte es pro Dokument nur einmal geben, der Selektor trifft also auf genau ein Element zu. Klassen können mehrfach vorkommen, aber ihr Vorkommen ist doch oft auf einen kleinen einstelligen Bereich begrenzt. Die Mehrzahl der Tags dagegen kommt sehr oft in einem Dokument vor. So sind alleine auf dieser Seite um die 500 span-Tags gesetzt, der Selektor #header .login span würde also initial 500 Elemente selektieren, nur um nach und nach 499 davon zu verwerfen. Noch problematischer ist die letzte Kategorie, da hier potentiell alle Elemente einer Seite geprüft werden müssen.

Bei allen Überlegungen zur Geschwindigkeit sollte man allerdings nicht die Les- und Wartbarkeit des HTML- und CSS-Codes aus den Augen verlieren. Moderne Browser werden immer effizienter darin, CSS-Regeln aufzulösen, sodass zwischen einem nicht optimierten und einem perfekt angepassten CSS-Layout am Schluss vielleicht doch nur 100 Millisekunden Unterschied für eine ganze Website liegen. Dafür die Erweiterbarkeit und Pflegbarkeit seiner Website einzuschränken sollte keine Option sein. Wie immer gilt also: Optimieren, wenn es nötig ist.

… „kurz mal was testen“

Zum Abschluss noch zwei Links, die vielleicht noch nicht jedem bekannt sind. Beim Arbeiten mit HTML, Javascript und CSS kommt man oft in die Situation, dass man nur kurz etwas in einer kleinen Sandbox ausprobieren will, wie zum Beispiel die oben vorgestellten Beispiele. Sich dafür eine separate Datei anzulegen, eventuell die Links für jQuery oder ähnliche Frameworks herauszusuchen und das Ganze immer und immer wieder im Browser zu laden, ist kein großer Aufwand, aber auf Dauer nervig. Genau das haben sich auch andere Leute gedacht und zu diesem Zweck Seiten wie JSFiddle oder JS-Bin geschaffen. Diese bieten die Möglichkeit HTML-, CSS- und Javascript-Code einzufügen, der direkt angezeigt wird. Zudem bieten beide schnellen Zugriff auf viele gängige Frameworks und fügen sie mit einem Klick in die Sandbox ein. Ideal also, um „mal eben kurz was zu testen“.

Merken

Merken