|
| 1 | +# Styles and classes |
| 2 | + |
| 3 | +Before we get into JavaScript's ways of dealing with styles and classes -- here's an important rule. Hopefully it's obvious enough, but we still have to mention it. |
| 4 | + |
| 5 | +There are generally two ways to style an element: |
| 6 | + |
| 7 | +1. Create a class in CSS and add it: `<div class="...">` |
| 8 | +2. Write properties directly into `style`: `<div style="...">`. |
| 9 | + |
| 10 | +JavaScript can modify both classes and `style` properties. |
| 11 | + |
| 12 | +We should always prefer CSS classes to `style`. The latter should only be used if classes "can't handle it". |
| 13 | + |
| 14 | +For example, `style` is acceptable if we calculate coordinates of an element dynamically and want to set them from JavaScript, like this: |
| 15 | + |
| 16 | +```js |
| 17 | +let top = /* complex calculations */; |
| 18 | +let left = /* complex calculations */; |
| 19 | + |
| 20 | +elem.style.left = left; // e.g '123px', calculated at run-time |
| 21 | +elem.style.top = top; // e.g '456px' |
| 22 | +``` |
| 23 | + |
| 24 | +For other cases, like making the text red, adding a background icon -- describe that in CSS and then add the class (JavaScript can do that). That's more flexible and easier to support. |
| 25 | + |
| 26 | +## className and classList |
| 27 | + |
| 28 | +Changing a class is one of the most often used actions in scripts. |
| 29 | + |
| 30 | +In the ancient time, there was a limitation in JavaScript: a reserved word like `"class"` could not be an object property. That limitation does not exist now, but at that time it was impossible to have a `"class"` property, like `elem.class`. |
| 31 | + |
| 32 | +So for classes the similar-looking property `"className"` was introduced: the `elem.className` corresponds to the `"class"` attribute. |
| 33 | + |
| 34 | +For instance: |
| 35 | + |
| 36 | +```html run |
| 37 | +<body class="main page"> |
| 38 | + <script> |
| 39 | + alert(document.body.className); // main page |
| 40 | + </script> |
| 41 | +</body> |
| 42 | +``` |
| 43 | + |
| 44 | +If we assign something to `elem.className`, it replaces the whole string of classes. Sometimes that's what we need, but often we want to add/remove a single class. |
| 45 | + |
| 46 | +There's another property for that: `elem.classList`. |
| 47 | + |
| 48 | +The `elem.classList` is a special object with methods to `add/remove/toggle` a single class. |
| 49 | + |
| 50 | +For instance: |
| 51 | + |
| 52 | +```html run |
| 53 | +<body class="main page"> |
| 54 | + <script> |
| 55 | +*!* |
| 56 | + // add a class |
| 57 | + document.body.classList.add('article'); |
| 58 | +*/!* |
| 59 | +
|
| 60 | + alert(document.body.className); // main page article |
| 61 | + </script> |
| 62 | +</body> |
| 63 | +``` |
| 64 | + |
| 65 | +So we can operate both on the full class string using `className` or on individual classes using `classList`. What we choose depends on our needs. |
| 66 | + |
| 67 | +Methods of `classList`: |
| 68 | + |
| 69 | +- `elem.classList.add/remove("class")` -- adds/removes the class. |
| 70 | +- `elem.classList.toggle("class")` -- adds the class if it doesn't exist, otherwise removes it. |
| 71 | +- `elem.classList.contains("class")` -- checks for the given class, returns `true/false`. |
| 72 | + |
| 73 | +Besides, `classList` is iterable, so we can list all classes with `for..of`, like this: |
| 74 | + |
| 75 | +```html run |
| 76 | +<body class="main page"> |
| 77 | + <script> |
| 78 | + for (let name of document.body.classList) { |
| 79 | + alert(name); // main, and then page |
| 80 | + } |
| 81 | + </script> |
| 82 | +</body> |
| 83 | +``` |
| 84 | + |
| 85 | +## Element style |
| 86 | + |
| 87 | +The property `elem.style` is an object that corresponds to what's written in the `"style"` attribute. Setting `elem.style.width="100px"` works the same as if we had in the attribute `style` a string `width:100px`. |
| 88 | + |
| 89 | +For multi-word property the camelCase is used: |
| 90 | + |
| 91 | +```js no-beautify |
| 92 | +background-color => elem.style.backgroundColor |
| 93 | +z-index => elem.style.zIndex |
| 94 | +border-left-width => elem.style.borderLeftWidth |
| 95 | +``` |
| 96 | + |
| 97 | +For instance: |
| 98 | + |
| 99 | +```js run |
| 100 | +document.body.style.backgroundColor = prompt('background color?', 'green'); |
| 101 | +``` |
| 102 | + |
| 103 | +````smart header="Prefixed properties" |
| 104 | +Browser-prefixed properties like `-moz-border-radius`, `-webkit-border-radius` also follow the same rule: a dash means upper case. |
| 105 | +
|
| 106 | +For instance: |
| 107 | +
|
| 108 | +```js |
| 109 | +button.style.MozBorderRadius = '5px'; |
| 110 | +button.style.WebkitBorderRadius = '5px'; |
| 111 | +``` |
| 112 | +```` |
| 113 | + |
| 114 | +## Resetting the style property |
| 115 | + |
| 116 | +Sometimes we want to assign a style property, and later remove it. |
| 117 | + |
| 118 | +For instance, to hide an element, we can set `elem.style.display = "none"`. |
| 119 | + |
| 120 | +Then later we may want to remove the `style.display` as if it were not set. Instead of `delete elem.style.display` we should assign an empty string to it: `elem.style.display = ""`. |
| 121 | + |
| 122 | +```js run |
| 123 | +// if we run this code, the <body> will blink |
| 124 | +document.body.style.display = "none"; // hide |
| 125 | + |
| 126 | +setTimeout(() => document.body.style.display = "", 1000); // back to normal |
| 127 | +``` |
| 128 | + |
| 129 | +If we set `style.display` to an empty string, then the browser applies CSS classes and its built-in styles normally, as if there were no such `style.display` property at all. |
| 130 | + |
| 131 | +````smart header="Full rewrite with `style.cssText`" |
| 132 | +Normally, we use `style.*` to assign individual style properties. We can't set the full style like `div.style="color: red; width: 100px"`, because `div.style` is an object, and it's read-only. |
| 133 | + |
| 134 | +To set the full style as a string, there's a special property `style.cssText`: |
| 135 | + |
| 136 | +```html run |
| 137 | +<div id="div">Button</div> |
| 138 | + |
| 139 | +<script> |
| 140 | + // we can set special style flags like "important" here |
| 141 | + div.style.cssText=`color: red !important; |
| 142 | + background-color: yellow; |
| 143 | + width: 100px; |
| 144 | + text-align: center; |
| 145 | + `; |
| 146 | +
|
| 147 | + alert(div.style.cssText); |
| 148 | +</script> |
| 149 | +``` |
| 150 | + |
| 151 | +This property is rarely used, because such assignment removes all existing styles: it does not add, but replaces them. May occasionally delete something needed. But we can safely use it for new elements, when we know we won't delete an existing style. |
| 152 | + |
| 153 | +The same can be accomplished by setting an attribute: `div.setAttribute('style', 'color: red...')`. |
| 154 | +```` |
| 155 | +
|
| 156 | +## Mind the units |
| 157 | +
|
| 158 | +Don't forget to add CSS units to values. |
| 159 | +
|
| 160 | +For instance, we should not set `elem.style.top` to `10`, but rather to `10px`. Otherwise it wouldn't work: |
| 161 | +
|
| 162 | +```html run height=100 |
| 163 | +<body> |
| 164 | + <script> |
| 165 | + *!* |
| 166 | + // doesn't work! |
| 167 | + document.body.style.margin = 20; |
| 168 | + alert(document.body.style.margin); // '' (empty string, the assignment is ignored) |
| 169 | + */!* |
| 170 | +
|
| 171 | + // now add the CSS unit (px) - and it works |
| 172 | + document.body.style.margin = '20px'; |
| 173 | + alert(document.body.style.margin); // 20px |
| 174 | +
|
| 175 | + alert(document.body.style.marginTop); // 20px |
| 176 | + alert(document.body.style.marginLeft); // 20px |
| 177 | + </script> |
| 178 | +</body> |
| 179 | +``` |
| 180 | +
|
| 181 | +Please note: the browser "unpacks" the property `style.margin` in the last lines and infers `style.marginLeft` and `style.marginTop` from it. |
| 182 | +
|
| 183 | +## Computed styles: getComputedStyle |
| 184 | +
|
| 185 | +So, modifying a style is easy. But how to *read* it? |
| 186 | +
|
| 187 | +For instance, we want to know the size, margins, the color of an element. How to do it? |
| 188 | +
|
| 189 | +**The `style` property operates only on the value of the `"style"` attribute, without any CSS cascade.** |
| 190 | +
|
| 191 | +So we can't read anything that comes from CSS classes using `elem.style`. |
| 192 | +
|
| 193 | +For instance, here `style` doesn't see the margin: |
| 194 | +
|
| 195 | +```html run height=60 no-beautify |
| 196 | +<head> |
| 197 | + <style> body { color: red; margin: 5px } </style> |
| 198 | +</head> |
| 199 | +<body> |
| 200 | +
|
| 201 | + The red text |
| 202 | + <script> |
| 203 | +*!* |
| 204 | + alert(document.body.style.color); // empty |
| 205 | + alert(document.body.style.marginTop); // empty |
| 206 | +*/!* |
| 207 | + </script> |
| 208 | +</body> |
| 209 | +``` |
| 210 | +
|
| 211 | +...But what if we need, say, to increase the margin by `20px`? We would want the current value of it. |
| 212 | +
|
| 213 | +There's another method for that: `getComputedStyle`. |
| 214 | +
|
| 215 | +The syntax is: |
| 216 | +
|
| 217 | +```js |
| 218 | +getComputedStyle(element, [pseudo]) |
| 219 | +``` |
| 220 | +
|
| 221 | +element |
| 222 | +: Element to read the value for. |
| 223 | +
|
| 224 | +pseudo |
| 225 | +: A pseudo-element if required, for instance `::before`. An empty string or no argument means the element itself. |
| 226 | +
|
| 227 | +The result is an object with styles, like `elem.style`, but now with respect to all CSS classes. |
| 228 | +
|
| 229 | +For instance: |
| 230 | +
|
| 231 | +```html run height=100 |
| 232 | +<head> |
| 233 | + <style> body { color: red; margin: 5px } </style> |
| 234 | +</head> |
| 235 | +<body> |
| 236 | +
|
| 237 | + <script> |
| 238 | + let computedStyle = getComputedStyle(document.body); |
| 239 | +
|
| 240 | + // now we can read the margin and the color from it |
| 241 | +
|
| 242 | + alert( computedStyle.marginTop ); // 5px |
| 243 | + alert( computedStyle.color ); // rgb(255, 0, 0) |
| 244 | + </script> |
| 245 | +
|
| 246 | +</body> |
| 247 | +``` |
| 248 | +
|
| 249 | +```smart header="Computed and resolved values" |
| 250 | +There are two concepts in [CSS](https://drafts.csswg.org/cssom/#resolved-values): |
| 251 | +
|
| 252 | +1. A *computed* style value is the value after all CSS rules and CSS inheritance is applied, as the result of the CSS cascade. It can look like `height:1em` or `font-size:125%`. |
| 253 | +2. A *resolved* style value is the one finally applied to the element. Values like `1em` or `125%` are relative. The browser takes the computed value and makes all units fixed and absolute, for instance: `height:20px` or `font-size:16px`. For geometry properties resolved values may have a floating point, like `width:50.5px`. |
| 254 | +
|
| 255 | +A long time ago `getComputedStyle` was created to get computed values, but it turned out that resolved values are much more convenient, and the standard changed. |
| 256 | +
|
| 257 | +So nowadays `getComputedStyle` actually returns the resolved value of the property, usually in `px` for geometry. |
| 258 | +``` |
| 259 | +
|
| 260 | +````warn header="`getComputedStyle` requires the full property name" |
| 261 | +We should always ask for the exact property that we want, like `paddingLeft` or `marginTop` or `borderTopWidth`. Otherwise the correct result is not guaranteed. |
| 262 | +
|
| 263 | +For instance, if there are properties `paddingLeft/paddingTop`, then what should we get for `getComputedStyle(elem).padding`? Nothing, or maybe a "generated" value from known paddings? There's no standard rule here. |
| 264 | +
|
| 265 | +There are other inconsistencies. As an example, some browsers (Chrome) show `10px` in the document below, and some of them (Firefox) -- do not: |
| 266 | +
|
| 267 | +```html run |
| 268 | +<style> |
| 269 | + body { |
| 270 | + margin: 10px; |
| 271 | + } |
| 272 | +</style> |
| 273 | +<script> |
| 274 | + let style = getComputedStyle(document.body); |
| 275 | + alert(style.margin); // empty string in Firefox |
| 276 | +</script> |
| 277 | +``` |
| 278 | +```` |
| 279 | + |
| 280 | +```smart header="Styles applied to `:visited` links are hidden!" |
| 281 | +Visited links may be colored using `:visited` CSS pseudoclass. |
| 282 | + |
| 283 | +But `getComputedStyle` does not give access to that color, because otherwise an arbitrary page could find out whether the user visited a link by creating it on the page and checking the styles. |
| 284 | + |
| 285 | +JavaScript may not see the styles applied by `:visited`. And also, there's a limitation in CSS that forbids applying geometry-changing styles in `:visited`. That's to guarantee that there's no side way for an evil page to test if a link was visited and hence to break the privacy. |
| 286 | +``` |
| 287 | +
|
| 288 | +## Summary |
| 289 | +
|
| 290 | +To manage classes, there are two DOM properties: |
| 291 | +
|
| 292 | +- `className` -- the string value, good to manage the whole set of classes. |
| 293 | +- `classList` -- the object with methods `add/remove/toggle/contains`, good for individual classes. |
| 294 | +
|
| 295 | +To change the styles: |
| 296 | +
|
| 297 | +- The `style` property is an object with camelCased styles. Reading and writing to it has the same meaning as modifying individual properties in the `"style"` attribute. To see how to apply `important` and other rare stuff -- there's a list of methods at [MDN](mdn:api/CSSStyleDeclaration). |
| 298 | +
|
| 299 | +- The `style.cssText` property corresponds to the whole `"style"` attribute, the full string of styles. |
| 300 | +
|
| 301 | +To read the resolved styles (with respect to all classes, after all CSS is applied and final values are calculated): |
| 302 | +
|
| 303 | +- The `getComputedStyle(elem, [pseudo])` returns the style-like object with them. Read-only. |
0 commit comments