Stop using ID’s in CSS !

Wie CSS gut funktioniert und warum es sinnvoll ist, keine ID's für Stylesheets zu verwenden.

Viele Webentwickler, die ich während meiner HTML5-Trainings nach ihrer Erfahrung bei Webtechnologien frage, geben bei Stylesheets die schlechteste Einschätzung ab, selbst wenn sie sich bei HTML oder Javascript als erfahren einstufen.

"Warum?", frage ich dann und ich bekomme häufig die gleiche Antwort. "Ich habe ein Projekt geerbt und muss nun Änderungen umsetzen. Ich verstehe nicht, wo ich das CSS verändern muss. Ich suche oft stundenlang herum, die Zeit habe ich aber nicht! Am Ende schreibe ich ein !important ..."

Die Wartbarkeit der meisten Stylesheets ist miserabel

Stylesheets erreichen irgendwann im Laufe der Zeit einen Zustand der Unwartbarkeit. Jede weitere Änderung fordert vom Entwickler entweder stundenlanges Forschen in CSS Zeilen oder aber den gekonnten, zunächst zeitsparenden Einsatz eines gepflegten !important. Damit wird die Unwartbarkeit allerdings endgültig zementiert, denn nach einem !important lässt sich nichts mehr anfügen.

Und die Ansprüche sind hoch

Die Ansprüche an Stylesheets sind komplex: am liebsten soll es ein responsiver Baukasten sein, in dem beliebige HTML-Layouts mit wenigen Klassen hergestellt werden können. Darüberhinaus soll er im Laufe der Monate und Jahre wachsen können. Ach ja, und jeder im Team soll sofort mitarbeiten können.

Ich zeige, wie man mit Selektoren und Gewichtungen eine skalierbare und wartbare CSS-Architektur aufsetzt. In diesem Artikel geht es um die grundlegendsten Prinzipien von CSS, um Kaskaden und Gewichtungen.

Ein kurzer Blick zurück

Cascading Stylesheets sind eine W3C Spezifikation aus dem Jahr 1996. Sie gehen auf einen Vorschlag Idee des Norwegers Håkon Wium Lie von 1994 zurück, der zusammen mit Tim Berners-Lee und Bert Bos an den Grundlagen von HTML und CSS gearbeitet hat.

Offensichtlich waren HTML und der Browser allerdings schneller als die Entwicklung von Style Sheets, denn die ersten Webseiten kamen ohne Style Sheets und sahen so aus:

<html>
    <head> ... </head>
    <body bgcolor="#fff" >
        <h1>Headline Fusce Fermentum</h1>
        <p><font face="Arial" size="2">Lorem ipsum dolor sit.</font></p>
        <p><font face="Arial" size="2">Egestas Ligula Aenean Adipiscing.</font></p>
        <table border="1" cellpadding="0">
            <tr>
                <td><font face="Arial" size="2">Bibendum Sollicitudin</font></td>
                <td><font face="Arial" size="2">Risus Sollicitudin</font></td>
            </tr>
        </table>
    </body>
</html>

Alle grafischen Entscheidungen wurden direkt ins HTML geschrieben. Die Angaben waren so gerade mal für das aktuelle Dokument gültig und mussten auf jeder weiteren Seite wiederholt werden. Auch innerhalb der Dokumente mussten beispielsweise Schriftangaben für jedes Element wiederholt werden.

Stylesheets vereinfachen die grafische Umsetzung

Die Idee wurde u.a. auf der World-Wide Web Konferenz im Frühjahr 1995 in groben Zügen von Bert Bos in einem Papier mit dem Titel Simple Style Sheets For SGML & HTML On The Web vorgestellt.

Assumptions (1)

  1. Addressable units
    • whole elements only
    • by context or by ID
  2. style sheet is static (declarative)
  3. style sheet consists of rules; rules consist of: selector + property + value; value may be an expression
  4. attributes not in selector, but in expression

Assumptions (2)

  1. text is rendered roughly in the order it is received (exception: tables, math)
  2. only appearrance specified, not interaction
  3. SGML, not just HTML

Der Autor dieses Papiers war offensichtlich ein kluger Kopf, denn die Annahmen wurden umgesetzt und sind bis heute gültig.

Elemente auswählen und einstellen

Zunächst das offensichtliche: Die grafischen Eigenschaften werden aus dem HTML Dokuments ausgekoppelt. Wenn visuelle Entscheidungen extern notiert sind und per <link> referenziert weden, dann können sie für beliebig viele HTML-Dokumente verwendet werden. Das vereinfacht das Refactoring ungemein. Das Aussehen aller Textabsätze <p>kann in einer Deklaration beschrieben werden.

 body { background-color:#fff; } 
 table { border: 1px solid black; }
 td { padding:2px; }
 p, td { font-family:Arial; font-size:1rem; }
<html>
    <head>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <h1>Headline Fusce Fermentum</h1>
        <p>Lorem ipsum dolor sit.</p>
        <p>Egestas Ligula Aenean Adipiscing.</p>
        <table>
            <tr>
                <td>Bibendum Sollicitudin</td>
                <td>Risus Sollicitudin</td>
            </tr>
        </table>
    </body>
</html>

Klassen, Attribute und Pseudoklassen

Um Textabsätze mit anderen grafischen Eigenschaften zu ermöglichen, gibt es Klassen. Sie lassen sich mit Formatvorlagen aus der Textverarbeitung vergleichen. Eine Klasse kann beliebige Eigenschaften bekommen und sie wird per HTML-Attribut <p class="...">zugewiesen. Die Klasse funktioniert wie ein Schalter,der Eigenschaften hinzufügt.

/* Element Selector */
 p, td { font-family:Arial; font-size:1rem; }

 /* Class Selector */
 p.teaser { font-size: 1.2rem } 
<html>
    <head>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <h1>Headline Fusce Fermentum</h1>
        <p class="teaser">Lorem ipsum dolor sit.</p>
        <p>Egestas Ligula Aenean Adipiscing.</p>
        <table>
            <tr>
                <td>Bibendum Sollicitudin</td>
                <td>Risus Sollicitudin</td>
            </tr>
        </table>
</html>

Bei anderen Elementen, wie zum Beispiel dem Anchor-Tag <a> ist es sinnvoll, nach Attributen zu unterscheiden. So gibt es Links <a href="..."> und benannte Anker <a name="...">. Beide nutzen dasselbe Tag, erfüllen aber eine unterschiedliche Aufgabe, je nach verwendetem Attribut. Wenn man Links also mit grafischen Attributen ausstatten möchte, dann möchte man dafür auf keinen Fall einen benannten Anker erwischen. Um jetzt nicht mit Klassen arbeitn zu müssen, kann man stattdessen das Attribut href ansprechen.

<a name="top"></a>

<a href="#top">jump to top</a>
a[href] { color: blue; }

Das Beispiel scheint unnötig, da der benannte Anchor sowieso nicht sichtbar ist, solange nichts darin steht. Aber was wäre bei ... a { display:block; min-height: 10px; min-width:40px; background-color: black; color: white; }?

Attributselektoren ermöglichen einen (immer noch) allgemeinen Zugriff auf HTML Elemente ohne mit Klassen arbeiten zu müssen.

Dasselbe gilt auch für Zustandsattribute wie a[href]:visited, a[href]:active, a[href]:hover und alle andere Variationen von sogenannten Pseudoklassen. Sie beschreiben ein Ankerelement in seinen verschiedenen Zustände während einer Interaktion.

a[href]:visited { color: purple; }
a[href]:active { color: red; }
a[href]:hover { color: blue; }

ID Selektoren

In "Simple Style Sheets" steht:

  1. Addressable units
    • whole elements only
    • by context or by ID

Es war (und ist) ein gängiges Konzept, DOM Element anhand von ID zu selektieren. Der ID Selektor #my-id {}referenziert ein einzelnes Element anhand seiner Id.

Das hat den Vorteil, ein Element und seine Nachfahrenelemente anhand eines eindeutigen Wurzelelements auszuwählen. Bei der Unterscheidung von mehreren Formularen innerhalb einer Webseite ist das hilfreich.

<form id="form-login"> 
    ...
    <button type="submit">enter</button>
</form>
#form-login {
    margin: 0.5rem;
}
#form-login button[type=submit] {
    ...
}

Soweit. So gut. In etwa ist es das, was man grundsätzlich über CSS Selektoren wissen sollte. Für den Rest dieses Artikels zumindest reicht das.

Kaskade

Das letzte gewinnt, die Kaskade bestimmt die Gültigkeit, oder?

Nun heisst es Cascading Style Sheets, "herabfallend" oder auch "Treppe". Deklarationen fallen eine Treppe herunter, um, unten angekommen, einem Dom Element sein Aussehen mitzuteilen. Aber nicht alle Regeln, die am Beginn der Treppe starten, kommen unten an. Manche werden unterwegs durch andere überschrieben

Eine CSS-Deklaration kann sich demnach auf drei Arten auswirken:

  1. sie definiert, wenn ein Element (per Selektor) und eine Eigenschaft das erste Mal angesprochen werden,
  2. sie ergänzt, wenn das Element bereits selektiert wurde, aber die Eigenschaft das erste Mal angesprochen wird,
  3. sie überschreibt, wenn das Element bereits selektiert und die Eigenschaft bereits definiert wurde.
p, td { font-family: Arial; }
p { font-size: 1rem; }
td { font-size: 0.95rem; }
p.teaser { font-size: 1.2rem; }
  • Definition: Für <p> und <td>wird zunächst eine Schriftart festgelegt.
  • Ergänzen: Die Schriftgröße wird für beide Element einzeln und verschieden ergänzt.
  • Überschreiben: Der Absatz für den Teaser bekommt eine größere Schrift und überschreibt deshalb die <p>-Schriftgröße.

Aber ist die Kaskade hier wirklich wichtig? Tatsächlich überhaupt nicht. Die Styles funktionieren auch in einer anderen Reihenfolge.

p.teaser { font-size: 1.2rem; }
p { font-size: 1rem; }
td { font-size: 0.95rem; }
p, td { font-family: Arial; }

Ergänzungen, wie font-familyzu font-size sind konfliktfrei, und deshalb ist die Reihenfolge innerhalb der CSS Datei unwichtig. Aber wie verhält sich mit der Überschneidung von p.teaserzu p? Hier wird in beiden Fällen font-sizedefiniert. Nach der Kaskadenregel gewinnt das zuletzt genannte und beansprucht die Schriftgröße für sich. In der praktischen Prüfung merken wir aber, dass die Klasse p.teaser ausgeführt wird und nicht der einfache p Elemente-Selektor, obwohl er später kommt.

Kaskaden werden schnell komplex

Kaskaden entstehen aus unterschiedlichen Gründen und sie können mit unterschiedlichen Zielsetzungen genutzt werden.

  • Innerhalb einer einzigen Datei folgen Deklarationen aufeinander: Spezifikation von Zuständen und Variationen
  • Mehrere Stylesheet Dateien bilden eine kaskadierende Folge: Aufteilung nach Aufgabe oder Kategorie.

Komplexe CSS Kaskaden

Warum wiegt eine Klasse schwerer als ein Element?

Ich denke, es ist ursprünglich wohl eine zweckmäßige Entscheidung: Klassen müssen Elemente überwiegen. Schließlich spezifizieren sie besondere Fälle für allgemeine Elemente. Dasselbe gilt für ID-Selektoren, die, weil sie an einzeln genannte Elemente gerichtet sind, noch mehr Gewicht brauchen, als Klassen oder eben Elemente.

Das W3C schreibt dazu in Calculating a Selectors Specifity:

A selector's specificity is calculated as follows:

  • count the number of ID selectors in the selector (= a)
  • count the number of class selectors, attributes selectors, and pseudo-classes in the selector (= b)
    count the number of type selectors and pseudo-elements in the selector (= c)
  • ignore the universal selector
    Selectors inside the negation pseudo-class are counted like any other, but the negation itself does not count as a pseudo-class.

Concatenating the three numbers a-b-c (in a number system with a large base) gives the specificity.

Examples:

*               /* a=0 b=0 c=0 -> specificity =   0 */
LI              /* a=0 b=0 c=1 -> specificity =   1 */
UL LI           /* a=0 b=0 c=2 -> specificity =   2 */
UL OL+LI        /* a=0 b=0 c=3 -> specificity =   3 */
H1 + *[REL=up]  /* a=0 b=1 c=1 -> specificity =  11 */
UL OL LI.red    /* a=0 b=1 c=3 -> specificity =  13 */
LI.red.level    /* a=0 b=2 c=1 -> specificity =  21 */
#x34y           /* a=1 b=0 c=0 -> specificity = 100 */
#s12:not(FOO)   /* a=1 b=0 c=1 -> specificity = 101 */

Gewichtungen

Elemente wiegen demnach 1`, Klassen, Pseudoklassen und Attributselektoren wiegen `10`, ID-Selektoren wiegen `100. Ausserdem addieren sie sich. Im vorausgegangenen Beispiel finden wir also folgende Gewichtungen:

p { font-size: 1rem; }                  /* specifity =   1 */
td { font-size: 0.95rem; }              /* specifity =   1 */
p, td { font-family: Arial; }           /* specifity =   1 */
p.teaser { font-size: 1.2rem; }         /* specifity =  11 */
#form-login { margin: 0.5rem; }         /* specifity = 100 */
#form-login button[type=submit] { ... } /* specifity = 111 */

Warum sind ID Selektoren keine gute Idee?

Für einen ID-Selektor gilt im Grunde keine Kaskade mehr, denn er ist mit einer Gewichtung von mindestens 100 so schwer, dass er sich nur noch mit wenigen kaskadierenden weiteren Selektoren überschreiben lässt. Es gibt jetzt nur noch die !important Ergänzung oder ein inline-style-Attribut mit einer höheren Gewichtung. Siehe auch CSS2 Gewichtungen berechnen.

Ein inline-styleAttribut hat eine Gweichtung von 1000und eine !importantKlausel hat effektiv eine Gewichtung von 10000.

#form-login button[type=submit] { color: lightsteelblue; } /* specifity =    111 */
button[type=submit] { color: steelblue; }                  /* specifity =     11 */
button[type=submit] { color: steelblue !important; }       /* specifity =  10011 */
<!-- specifity = 1000  -->
<button type="submit" style="color: steelblue;">enter</button>

Im Arbeitsalltag fordern ID Selektoren die Verwendung von Inlinestyles und !important!Regeln heraus. Danach sind dann aber für ein weiteres Refactoring keine weiteren Türen mehr offen. Jeder, der mit über die Jahre gewachsenen Webtemplates arbeitet, kennt das Problem. Und es gibt keine Lösung dafür.

"Nach mir die Sintflut" ("devil may care") beschreibt den Zustand ziemlich genau.

Cascading statt Gewichtung!

"Cascading Style Sheet" meint im Originaltext

Cascading order

To find the value for an element/property combination, user agents must apply the following sorting order:

  1. Find all declarations that apply to the element and property in question, for the target media type. Declarations apply if the associated selector matches the element in question and the target medium matches the media list on all @media rules containing the declaration and on all links on the path through which the style sheet was reached.
  2. Sort according to importance (normal or important) and origin (author, user, or user agent). In ascending order of precedence:
    1. user agent declarations
    2. user normal declarations
    3. author normal declarations
    4. author important declarations
    5. user important declarations
  3. Sort rules with the same importance and origin by specificity of selector: more specific selectors will override more general ones. Pseudo-elements and pseudo-classes are counted as normal elements and classes, respectively.
  4. Finally, sort by order specified: if two declarations have the same weight, origin and specificity, the latter specified wins. Declarations in imported style sheets are considered to be before any declarations in the style sheet itself.

Don't use #-Selectors.

Kaskade und Gewichtungen lassen sich auch in komplexeren Architekturen völlig problemlos vereinen, solange man nur ID-Selektoren weglässt! !important Klauseln dagegen sind eine sinnvolle Ergänzung, denn sie sichern ab, dass sich verändernde Zustände auch zuverlässig angezeigt werden.

Kaskadierende Architektur und Gewichtungen, kombiniert.