forked from DivadNojnarg/outstanding-shiny-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
beautify-css.Rmd
1188 lines (900 loc) · 44.7 KB
/
beautify-css.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# CSS for Shiny {#beautify-css}
In the previous part, we saw how to create and manipulate HTML tags
with `{shiny}` and `{htmltools}`, as well as importing external web dependencies, especially CSS files. This is, however, far from being enough to develop apps that stand out from the pack. As introduced in \@ref(html-and-css), __CSS__ is a web language allowing a deep customization of the appearance. This part aims at providing an acceptable overview of CSS capabilities in the Shiny context, so that you may start to seamlessly and quickly __customize__ your apps.
## How to include CSS?
There are three ways to include CSS in a web page:
- Point to an external file `<link rel="stylesheet" href="style.css"/>`.
- Insert the style in the `head`. Not recommended for complex projects since this is hard to maintain.
- Insert the style at the tag level, through the `style` attribute, also known as __inline__ CSS,
and this is not recommended since it is hard to maintain.
```{r, echo=FALSE, results='asis'}
html_code <- '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="style.css" />
<title>CSS</title>
<style type="text/css">
p {
color: red;
}
</style>
</head>
<body>
<p style="color: red">Hello World</p>
<p>Some text</p>
<div>Plop</div>
</body>
</html>'
code_chunk_custom(html_code, "html")
```
We advise you to follow the first method as much as you can so that you modify the CSS in only one place!
At the end of the day, your CSS script will look like the following example:
```{r, echo=FALSE, results='asis'}
css_code <- "tag1 {
property1: value1;
property2: value2;
}
tag2 {
property1: value1;
}"
code_chunk_custom(css_code, "css")
```
All of the above methods apply to Shiny:
- __External__ CSS file `tags$link(rel = "stylesheet", type="text/css", href="www/style.css")`.
- __Internal__ CSS `tags$head(tags$style("p {color: red;}"))`.
- __Inline__ CSS `p(style = "color:red;", "Red text")`.
However, if you correctly followed the previous part about HTML dependencies (see Chapter \@ref(htmltools-dependencies)), the best way in Shiny to create a dependency and attach it to a tag is:
```{r, eval=FALSE}
css_dep <- function() {
htmlDependency(
name = "css dep",
version = "1.0",
src = path_to_css,
stylesheet = filename
)
}
tagList(tag, css_dep)
```
Following this method, you make the file accessible on the server, thereby easing the debugging process. When should we apply the above method? Well, it does not make a lot of sense for tiny CSS modifications, where you could use one of the three other methods listed previously. Moreover, it is also fine for an external dependency for which you don't need
to debug or modify the code.
## CSS selectors {#css-selectors}
CSS __selectors__ define on which elements to apply CSS rules. Below, we review the most common patterns.
### Basics
To apply CSS, we select a tag, choose the property we want to change and give it a value:
```{r, echo=FALSE, results='asis'}
css_code <- "selector {
property: value;
}"
code_chunk_custom(css_code, "css")
```
For instance, to set the color blue to the `p` tag, we would do:
```{r, echo=FALSE, results='asis'}
css_code <- "p {
color: red;
}"
code_chunk_custom(css_code, "css")
```
Now, how would we apply the same property to multiple tags? We separate tags by a comma, also called a __grouping__ selector:
```{r, echo=FALSE, results='asis'}
css_code <- "p, div {
color: red;
}"
code_chunk_custom(css_code, "css")
```
See below an example with a Shiny app:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/selector-basics"), "r")
```
### Select by class, id
The method shown above applies to all selected tags. This is not necessarily what we want as we probably would like to filter. We first add a `class` to the first `p` tag. Best practice is to give it a meaningful name:
```{r, echo=FALSE, results='asis'}
html_code <- '<p class="first-p"></p>'
code_chunk_custom(html_code, "html")
```
On the CSS side, we prefix the class by a `.`:
```{r, echo=FALSE, results='asis'}
css_code <- ".first-p {
color: red;
}"
code_chunk_custom(css_code, "css")
```
To style unique elements, we may use `id`:
```{r, echo=FALSE, results='asis'}
css_code <- "#element {
color: red;
}"
code_chunk_custom(css_code, "css")
```
As another example, the result is shown in Figure \@ref(fig:css-simple-select):
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/selector-class-id"), "r")
```
The second `p` tag is not selected.
```{r css-simple-select, echo=FALSE, fig.cap='Select by class and id.', out.width='75%', fig.align='center'}
knitr::include_graphics("images/beautify/css-simple-select.png")
```
### Apply CSS to single elements
Let's consider the following example:
```{r, echo=FALSE, results='asis'}
html_code <- '<p>Hello World! What\'s up?</p>'
code_chunk_custom(html_code, "html")
```
How would you change the color of the `World` word? We could not select the whole `p` element, as it would apply to the
whole tag. There exists specific tags like `div` and `span` that you can apply. As stated in Chapter \@ref(web-intro-html), `span` may be used inside containers to surround words:
```{r, echo=FALSE, results='asis'}
html_code <- '<p>Hello <span class="custom">World</span>! What\'s up?</p>'
code_chunk_custom(html_code, "html")
```
```{r, echo=FALSE, results='asis'}
css_code <- ".custom {
color: red;
}"
code_chunk_custom(css_code, "css")
```
### Advanced selectors
Until now, we've seen how to select an element by the tag name, a class and an id. Yet, this is not enough. How would you select the below tag?
```{r, echo=FALSE, results='asis'}
html_code <- '<a data-toggle="dropdown">Tag</a>'
code_chunk_custom(html_code, "html")
```
#### Select nested tags
We consider two HTML tags. We only want the first `a` element to have red text.
We can't select by class `.nav-link` as it would also style the second element!
```{r, echo=FALSE, results='asis'}
html_code <- '<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<a class="nav-link" href="#">Link</a>'
code_chunk_custom(html_code, "html")
```
The idea is to select the first tag, that is `li`, then select its child `a`:
```{r, echo=FALSE, results='asis'}
css_code <- "li a {
color: red;
}"
code_chunk_custom(css_code, "css")
```
The Shiny `navbarPage` template is composed of navigation items `li` and `a`, which we may easily modify:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/selector-nested/ex1"), "r")
```
```{r css-combined-selectors-1, echo=FALSE, fig.cap='Style all nav link elements.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/beautify/css-combined-selectors-1.png")
```
You'll notice that `tabsetPanel()` tabs are also modified, which is not exactly what we wanted, as shown in Figure \@ref(fig:css-combined-selectors-1). The main difference between `navbarPage()` and `tabsetPanel()` is the class held by the the menu wrapper `ul`:
```{r, eval=FALSE}
navbarPage(tabPanel("test"))
```
```{r, echo=FALSE}
if (knitr::is_html_output()) {
navbarPage(tabPanel("test"))
}
```
```{r, echo=FALSE, results='asis'}
code <- '<nav class="navbar navbar-default navbar-static-top"
role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand">
<div class="tab-pane" title="test"
data-value="test"></div>
</span>
</div>
<ul class="nav navbar-nav" data-tabsetid="2341"></ul>
</div>
</nav>
<div class="container-fluid">
<div class="tab-content" data-tabsetid="2341"></div>
</div>'
exclude_from_html(code)
```
```{r, eval=FALSE}
tabsetPanel(tabPanel("test"))
```
```{r, echo=FALSE}
if (knitr::is_html_output()) {
tabsetPanel(tabPanel("test"))
}
```
```{r, echo=FALSE, results='asis'}
code <- '<div class="tabbable">
<ul class="nav nav-tabs" data-tabsetid="6625">
<li class="active">
<a href="#tab-6625-1" data-toggle="tab"
data-value="test">test
</a>
</li>
</ul>
<div class="tab-content" data-tabsetid="6625">
<div class="tab-pane active" data-value="test"
id="tab-6625-1">
</div>
</div>
</div>'
exclude_from_html(code)
```
which is `nav navbar-nav` for `navbarPage()` and `nav nav-tabs` for `tabsetPanel()`.
To isolate `navbarPage()` tabs, we have to improve our previous selector:
```{r, echo=FALSE, results='asis'}
css_code <- ".navbar-nav li a {
font-size: 20px;
font-weight: bold;
}"
code_chunk_custom(css_code, "css")
```
Doing so, we'll only look at the link elements inside the container having the `navbar-nav` class.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/selector-nested/ex2"), "r")
```
The final result is depicted in Figure \@ref(fig:css-combined-selectors-2).
```{r css-combined-selectors-2, echo=FALSE, fig.cap='Style specific navbar nav link elements.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/beautify/css-combined-selectors-2.png")
```
#### Select by attributes
To select by attribute, we use `tag[attribute]`:
```{r, echo=FALSE, results='asis'}
html_code <- '<a data-toggle="dropdown">Tag</a>
<a data-toggle="my dropdown">Tag</a>
<a data-toggle="whatever">Tag</a>'
code_chunk_custom(html_code, "html")
```
The below CSS code selects all `a` tags having a `data-toggle` attribute:
```{r, echo=FALSE, results='asis'}
css_code <- "a[data-toggle] {
color: red;
}"
code_chunk_custom(css_code, "css")
```
while the following code, will only select the first `a` tag:
```{r, echo=FALSE, results='asis'}
css_code <- "a[data-toggle=\"dropdown\"] {
color: red;
}"
code_chunk_custom(css_code, "css")
```
The reason is that we exactly match the `dropdown` value!
We could be less specific:
```{r, echo=FALSE, results='asis'}
css_code <- "a[data-toggle*=\"dropdown\"] {
color: red;
}"
code_chunk_custom(css_code, "css")
```
`*` checks whether the word `dropdown` is there but not the position. You may use `^` and `$` like you do with regular expressions to check if the value starts or ends by the specified word, respectively.
#### Other selectors
##### Direct descendants
Let's consider a `navbarPage()` with nested navigation. This is accomplished with the `navbarMenu()` function:
```{r, eval=FALSE}
navbarPage(
"App Title",
tabPanel("Plot"),
navbarMenu(
"More",
tabPanel("Summary"),
"----",
"Section header",
tabPanel("Table")
)
)
```
The resulting HTML code is as follow:
<!-- I copied an paste just for a formatting reasons
to avoid code from going outside margins in the pdf output -->
```{r, echo=FALSE}
if (knitr::is_html_output()) {
navbarPage(
"App Title",
tabPanel("Plot"),
navbarMenu(
"More",
tabPanel("Summary"),
"----",
"Section header",
tabPanel("Table")
)
)
}
```
```{r, echo=FALSE, results='asis'}
code <- '<ul class="nav navbar-nav" data-tabsetid="5879">
<li class="active">
<a href="#tab-5879-1" data-toggle="tab" data-value="Plot">
Plot
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"
data-value="More">
More
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="7141">
<li>
<a href="#tab-7141-1" data-toggle="tab"
data-value="Summary">Summary</a>
</li>
<li class="divider"></li>
<li class="dropdown-header">Section header</li>
<li>
<a href="#tab-7141-4" data-toggle="tab"
data-value="Table">Table</a>
</li>
</ul>
</li>
</ul>'
exclude_from_html(code)
```
There are two `ul` menus with multiple `li` and `a` inside. Applying our previous CSS selector as depicted in Figure \@ref(fig:css-direct-child-1) selects all links.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/selector-descendant"), "r")
```
```{r css-direct-child-1, echo=FALSE, fig.cap='Style all navbar nav link elements.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/beautify/css-direct-child-1.png")
```
What if we only want to select the direct `a` children of the outer menu, meaning we exclude the inner menu links? Using the __child combinator__ `>`, we can select direct children and not all descendants that could contain grand children.
```{r, echo=FALSE, results='asis'}
css_code <- ".navbar-nav > li > a {
font-size: 20px;
font-weight: bold;
}"
code_chunk_custom(css_code, "css")
```
Result is displayed in Figure \@ref(fig:css-direct-child-2):
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/selector-direct-descendant"), "r")
```
```{r css-direct-child-2, echo=FALSE, fig.cap='Style only direct children navbar nav link elements.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/beautify/css-direct-child-2.png")
```
Well, what if we want to be even more specific and get only the very first nav link element?
We have to introduce pseudo-classes.
##### Pseudo-classes {#css-pseudo-classes}
A __pseudo-class__ starts with the `:` symbol. For instance, `a:active` styles active links and `button:disabled` applies for disabled buttons.
```{r, echo=FALSE, results='asis'}
css_code <- "a:active {
}
button:disabled {
}
checkbox:checked {
}"
code_chunk_custom(css_code, "css")
```
There exists many pseudo-classes types, particularly the structural ones. Those are useful to select specific children like `element:first-child`. Going back to the `navbarPage()` example, we could refine our CSS selector adding `:first-child` to the `li` element as we want to only select the first item:
```{r, echo=FALSE, results='asis'}
css_code <- ".navbar-nav > li:first-child > a {
font-size: 20px;
font-weight: bold;
}"
code_chunk_custom(css_code, "css")
```
As expected, only the first link is styled, as demonstrated in Figure \@ref(fig:css-direct-child-3).
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/selector-pseudoclass"), "r")
```
```{r css-direct-child-3, echo=FALSE, fig.cap='Style only the first navbar nav link element.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/beautify/css-direct-child-3.png")
```
##### Pseudo-elements {#css-pseudoelements}
__Pseudo-elements__ are preceded by `::`. The most famous ones are `::before` and `::after`, that respectively apply before and after the selected element.
```{r, echo=FALSE, results='asis'}
css_code <- "a::before {
}
a::after {
}"
code_chunk_custom(css_code, "css")
```
For instance `.navbar-nav > li:first-child > a::before` will insert an element before the very first navbar link.
```{r css-pseudoelements-code, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/selector-pseudoelement"), "r")
```
As an exercise, you may change `a::before` to `a::after` and notice how the emoji behaves in the above example.
```{r css-pseudoelements-before, echo=FALSE, fig.cap='Add before pseudo-element.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/beautify/css-pseudoelements-before.png")
```
## Best practices
### DRY principle
__DRY__ stands for "Don't repeat yourself". This methodology not only applies to structural languages like R and JavaScript but also to declarative languages like CSS. Below is an example of badly managed CSS code. Both `.class-1` and `.class-2` have the same color and border radius. Only the padding and font size change. This type of code does not follow the DRY rule and increases the risk of having contradictory CSS properties.
```{r, echo=FALSE, results='asis'}
css_code <- ".class-1{
color: #33BEFF;
padding: 10px 16px;
font-size: 12px;
border-radius: 3px;
}
.class-2{
color: #33BEFF;
padding: 5px 10px;
font-size: 12px;
border-radius: 3px;
}"
code_chunk_custom(css_code, "css")
```
We could gather all common properties in one __generic class__:
```{r, echo=FALSE, results='asis'}
css_code <- ".generic {
color: #33BEFF;
border-radius: 3px;
}
.specific-1 {
padding: 10px 16px;
font-size: 18px;
}
.specific2 {
padding: 5px 10px;
font-size: 12px;
}"
code_chunk_custom(css_code, "css")
```
On the HTML side, we add classes from the most generic to the most __specific__ like:
```{r, echo=FALSE, results='asis'}
html_code <- '<div class="generic specific-1">My element</div>
<div class="generic specific-2">My element</div>'
code_chunk_custom(html_code, "html")
```
As an even more concrete example, take a look at the [bulma](https://bulma.io/documentation/elements/button/) button:
```{r, echo=FALSE, results='asis'}
html_code <- '<button class="button is-large is-fullwidth">Large</button>'
code_chunk_custom(html_code, "html")
```
Notice how easy it is to predict the style of the button. It will be large and take the maximum width. It is for sure more meaningful than seeing a `.class1`!
### Selector specificity
What happens in case multiple different selectors target the same element? Which style is really applied? Consider the following example:
```{r, echo=FALSE, results='asis'}
html_code <- '<div id="element" class="myclass">Some text</div>'
code_chunk_custom(html_code, "html")
```
with the following CSS rules:
```{r, echo=FALSE, results='asis'}
css_code <- "#element {
color: red;
}
.myclass {
color: blue;
}
div {
color: green;
}"
code_chunk_custom(css_code, "css")
```
Can you guess what would be the element text color? Well, this is not obvious. You must understand that web browsers apply rules to select the more specific rules:
- Inline style is the most specific. This is style directly attached to the HTML tag.
- `id` applies to unique objects.
- `class`, `pseudoclasses`, attributes.
- elements and pseudo-elements.
Going back to our previous example, the most specific selector is `#element`, therefore the text color will be red. What happens in case of equality? The last written selector is applied:
```{r, echo=FALSE, results='asis'}
html_code <- '<div class="class1 class2">Some text</div>'
code_chunk_custom(html_code, "html")
```
```{r, echo=FALSE, results='asis'}
css_code <- ".class1 {
color: blue;
}
.class2 {
color: red;
}"
code_chunk_custom(css_code, "css")
```
Only the second selector is applied to the tag. To apply `class1`, you must move it after the `class2` style definition.
Best practice is to keep the lowest specificity as possible, thereby making `.class` the first choice.
### Block element modified (BEM)
The BEM [methodology](http://getbem.com/introduction/) will help you to write meaningful CSS, easier to manage.
A block is a standalone entity having a self-explanatory name like `header`, `checkbox`. An elements is part of the block but not standalone like menu items.
A modifier indicates a specific state of an element, for instance if a button is disabled, active, ... The general scheme is defined below:
```{r, echo=FALSE, results='asis'}
css_code <- ".block__element--modifier {
/* rules */
}"
code_chunk_custom(css_code, "css")
```
Following this methodology guaranties you have documented and meaningful classes.
## Modify CSS with the HTML inspector
### Add inline properties
1. Run the following app:
<!-- We will not deploy this app. This is not informative -->
```{r, eval=FALSE}
ui <- fluidPage(
tags$style("p { color: red;}"),
p("Hello World")
)
server <- function(input, output, session) {}
shinyApp(ui, server)
```
2. Right-click the `p` element and open the inspector. In the `Styles` tab, notice the `element.style` section. This is to define inline new CSS rules for the selected item.
3. Let's add two rules by clicking inside the `element.style` container:
- `border-style: dashed`. Indicates we want a box with dashed border
- `border-color: ...`. To set a border color
After typing enter, the inline CSS is automatically added in the tag element.
You probably noticed the auto-suggestion feature while typing a property, as shown in Figure \@ref(fig:html-dom-css), being particularly handy when learning CSS.
```{r html-dom-css, echo=FALSE, fig.cap='Edit CSS with Chrome DevTools.', out.width='100%'}
knitr::include_graphics("images/survival-kit/dom-css.png")
```
### View local changes
Whenever playing around with a web page style or a Shiny app, it may be good to have an overview of all changes when you are satisfied about the new design.
1. Run the previous Shiny app example and open the inspector.
2. In the Styles tab, add some new CSS properties to the first p element set of rules.
3. Once done press enter and click on the file name located at the top-right corner of the property box.
4. This opens the Sources tab. In the left sidebar, right click on the file name (index) and select `Local Modifications`.
5. This opens a `Git` like diff panel called `Changes` where you can review any change and see the previous values. If you refresh the page, you will lose every local change, unless the persistent authoring feature is active.
```{r html-dom-css-changes, echo=FALSE, fig.cap='Review all CSS changes.', out.width='100%'}
knitr::include_graphics("images/survival-kit/dom-css-changes.png")
```
To get a deeper understanding of the different CSS options please refer to the Google [documentation](https://developers.google.com/web/tools/chrome-devtools/css).
## CSS in action
We review the most commonly used CSS properties such as text styling, shadows, color and background color, ...
### Text Styling
#### Fonts
__Fonts__ control the text appearance and may be changed as below:
```{r, echo=FALSE, results='asis'}
css_code <- "tag {
font-family: mypolice1, mypolice2, ..., serif;
}"
code_chunk_custom(css_code, "css")
```
In practice, we enter multiple font values, in case the end user does not have the same
font. The last values are composed of generic polices like `serif` and `monospace`.
For instance, Bootstrap 3 utilizes the default `sans-serif` police. A neat tool to inspect the current text fonts is the HTML inspector CSS Overview tab, as shown Figure \@ref(fig:css-overview-font-family).
```{r css-overview-font-family, echo=FALSE, fig.cap='Overview of the font properties.', out.width='100%'}
knitr::include_graphics("images/beautify/css-overview-font-family.png")
```
Custom fonts may be downloaded with `@font-face`, but there are few prerequisites to make them render properly:
- The file format must be correctly handled by the end-user web browsers. There are currently `.ttf` (works on almost all web browsers), `.woff` (works on almost all web browsers), `.svg` (iPhone and iPad), `.eot` (IE only) and `.otf` (doesn't work on IE).
- Those files have to be downloaded, which may take time.
- Most fonts are not open source.
```{r, echo=FALSE, results='asis'}
css_code <- "@font-face {
font-family: 'customPolice';
src: url('customPolice.ttf'),
url('customPolice.woff');
}"
code_chunk_custom(css_code, "css")
```
Importantly, the font files must be accessible to the CSS file where they are called from. As another example, the below code means that the `fonts` folder is located one level above the CSS file:
```{r, echo=FALSE, results='asis'}
css_code <- "@font-face {
font-family: 'Framework7 Icons';
font-style: normal;
font-weight: 400;
src: url(\"../fonts/Framework7Icons-Regular.eot\");
src: url(\"../fonts/Framework7Icons-Regular.woff2\"),
url(\"../fonts/Framework7Icons-Regular.woff\"),
url(\"../fonts/Framework7Icons-Regular.ttf\");
}"
code_chunk_custom(css_code, "css")
```
Chapters \@ref(beautify-with-bootstraplib) and \@ref(beautify-with-fresh) expose convenient methods to seamlessly handle Shiny app's font.
#### Size
To change the text size, we use the `font-size` property. From there, you have two choices:
- __Relative__ size (first choice) like `small`, `medium`, ...
You may also find notations like `1em` (default size), `1.8em` being bigger and `0.5em` being smaller; as well as percentages.
- __Absolute__ size: 16px, 20px, ...
In the following example, the second `h1` tag is smaller than the default one, so is `p`:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/font-size"), "r")
```
#### Alignment
This is a job for `text-align` which accepts four values (left, center, right and justify). Importantly, this does not work for inline tags like `span`!
#### Other styling properties
You can also put any text in bold, italic using `font-style: italic` and `font-weight: bold`, respectively.
### Colors
We have already mentioned this property before in the previous examples. In CSS, there are four ways to apply colors to a text or to the background:
- Using the __hexadecimal__ notation (HEX). For instance `#FFFFFF` corresponds to the white color. Given a color code, unless you already know it, the result is quite hard to predict. Could you guess what is the result of `#4b0082`?
- Using the __rgb__ code. If you ever followed art courses, this is the classic system used in painting, by mixing colors. `rgb` stands for red, green and blue. Each scale goes from 0 to 255 (256 choices), which gives a weight to each color! Why 255? Because `256*256*256` gives about 16 millions colors, the maximum number of colors that the actual screen can display.
- Using the __hsl__ convention. `hsl` stands for hue, saturation and lightness. Saturation and lightness are expressed in percentage from 0 to 100. The hue goes from red (0) to 360 (a sort of red) each time incrementing by 15. The yellow color is 60.
- Using the __color name__ like `ghostwhite`, `maroon`, `red`...
While `rgb` and `hsl` give an unlimited number of possibilities, HTML color names are not infinite.
There are many tools allowing you to get each of those codes, for example, color pickers:
- https://html-color.codes/
- https://www.w3schools.com/colors/colors_picker.asp
#### Text color
Given the methods listed above, we use the `color` property to change any text color. For obvious colors like `red`, this is not necessary to use the `rgb` convention.
```{r, echo=FALSE, results='asis'}
css_code <- "p {
color: red;
}"
code_chunk_custom(css_code, "css")
```
#### Background color
__backgound-color__ is the property necessary to apply a given color to the __background__. To change to page background color, we target the `body` tag, but you could also target any tag like `p`.
Colors may be seamlessly tweaked with the developer tools as shown later in Figure \@ref(fig:box-shadow-tool-2).
### Borders and shadows
These effects may be applied to text and block elements. The easiest way to set up a __border__ is to leverage the `border` property, which accepts three parameters:
- The border __width__ in px.
- The border __color__ with any valid CSS color like `rgb()`, `red`, ...
- The border __type__, which corresponds to the border style (`none`, `solid`,
`dotted`, `dashed`, `double`, `groove`, `ridge`, `inset` and `outset`). In practice, we mostly use `solid`.
There exists other properties such as __border-radius__, which controls the shape of the corners from top left to bottom left. The higher the value the more rounded is the corner.
If for any reason, you want to fine-tune the border position, there are four properties: __border-top__, __border-bottom__, __border-left__ and __border-right__.
Finally, __shadows__ are controlled by two properties:
- __text-shadow__, which applies a shadow to a text element (rarely used).
- __box-shadow__ for styling blocks.
For instance:
```{r, echo=FALSE, results='asis'}
css_code <- "block {
box-shadow: 2px 2px 0px black;
}"
code_chunk_custom(css_code, "css")
```
The `box-shadow` [property](https://www.w3schools.com/cssref/css3_pr_box-shadow.asp) takes four main parameters:
- The __horizontal offset__ in px.
- The __vertical offset__ in px.
- The __blur radius__ value in px. The higher the more blurred.
- The shadow color.
Do you remember the `{shinydashboard}` box that we imported inside a simple Shiny app in Chapter \@ref(htmldeps-import)? This box has been included in the `{OSUICode}` side package. Let's add some border and shadows:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/custom-box/start"), "r")
```
The `box` class is styled as follows and shown Figure \@ref(fig:box-shadow-1):
```{r, echo=FALSE, results='asis'}
css_code <- ".box {
border-radius: 3px;
border-top: 3px solid #d2d6de;
box-shadow: 0 1px 1px rgb(0 0 0 / 10%);
}"
code_chunk_custom(css_code, "css")
```
```{r box-shadow-1, echo=FALSE, fig.cap='{shinydashboard} box style.', out.width='100%'}
knitr::include_graphics("images/beautify/box-shadow-1.png")
```
The `border-radius` of 3px indicates that the box is slightly rounded on all corners.
There is a top solid border whose color is gray as well as a tiny shadow.
Let's increase the `border-radius` to 10px, change the top border to `border-left` with an orange color and slightly increased thickness:
```{r, echo=FALSE, results='asis'}
css_code <- ".box {
border-radius: 10px;
border-left: 6px solid #e28810;
box-shadow: 0 1px 1px rgb(0 0 0 / 10%);
}"
code_chunk_custom(css_code, "css")
```
We would like to increase the shadow effect only on mouse hover, with reasonable values. To proceed we leverage the pseudo format elements like `:hover`:
```{r, echo=FALSE, results='asis'}
css_code <- ".box:hover {
box-shadow: 0px 8px 8px 0px rgb(0, 0, 0, 0.2);
}"
code_chunk_custom(css_code, "css")
```
The easiest way to modify the shadow style is directly inside the HTML inspector, as demonstrated in Figure \@ref(fig:box-shadow-tool-1). Next to the property text, there is a button to open the shadow editor. We click on it, which opens a box with input to control the shadow offset, the blur and the spread parameter. We leave the effect outside the box, that is, `outset` is unchanged. To change the color, we click on the color widget next to the rgb value, which opens Figure \@ref(fig:box-shadow-tool-2).
```{r box-shadow-tool-1, echo=FALSE, fig.cap='Box shadow shape tools.', out.width='30%', fig.align='center'}
knitr::include_graphics("images/beautify/box-shadow-tool-1.png")
```
```{r box-shadow-tool-2, echo=FALSE, fig.cap='Box shadow color tools.', out.width='30%', fig.align='center'}
knitr::include_graphics("images/beautify/box-shadow-tool-2.png")
```
We finally include this new CSS rule inside the previous app. Optionally, we may change
the color of the text header to be white, to improve the contrast with the box background. (See Figure \@ref(fig:box-shadow-result)):
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/custom-box/end"), "r")
```
```{r box-shadow-result, echo=FALSE, fig.cap='Improved box shadow on hover.', out.width='50%', fig.align='center'}
knitr::include_graphics("images/beautify/box-shadow-result.png")
```
### Animations {#css-animations}
Properly utilized, CSS __animations__ are a powerful way to highlight a specific part of your app, without any line of JavaScript code. For instance, assume you have an application that requires users to click on a run button after filling some parameters. You may want to indicate to the user that they can click on it. However, I would recommend playing the animation __once__ or adding a disable option, as it might cause more __frustration__ if people visit your app quite frequently.
To create an animation, you have to leverage the `@keyframes` [rule](https://www.w3schools.com/css/css3_animations.asp). This basically describes what happens and when during the animation. For instance:
```{r, echo=FALSE, results='asis'}
css_code <- "@keyframes animation {
from {property: value;}
to {property: new_value;}
}"
code_chunk_custom(css_code, "css")
```
Alternatively, you may also use __percentages__, which is more flexible as you may introduce multiple
changes. The percentage is calculated based on the overall animation duration, that is, if the animation lasts 4 seconds, 50% corresponds to 2s:
```{r, echo=FALSE, results='asis'}
css_code <- "@keyframes my-animation {
0% {property: value1;}
25% {property: value2;}
50% {property: value3;}
/* ... */
100% {property: value4;}
}"
code_chunk_custom(css_code, "css")
```
We try below in a simple Shiny app, where we simply change the text color from black to red:
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/animations/basic-broken"), "r")
```
Nothing happens. Could you spot why?
We forgot to attach the animation to the element. You may use the `animation` super property or multiple properties as shown below:
```{r, echo=FALSE, results='asis'}
css_code <- "element {
animation: name, duration, delay, count, ...;
/* OR */
animation-name: ...,
animation-duration: ...,
/* ... */
}"
code_chunk_custom(css_code, "css")
```
If you want to keep the last state of the animation active, use `animation-fill-mode: forwards;`.
You also may want to give time to the user to open the page before playing the animation, especially if the latter has a short duration. Therefore, it is often advised to specify a __delay__ with `animation-delay`.
Let's try again below.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/animations/basic"), "r")
```
To close this section, I propose creating a very simple Shiny app that calculates the sum of two numbers. It relies on a run button to update the result. The goal is to make it blinking one second after the app starts, for 3 seconds. We first design the animation, calling it `blink`. We decide to utilize the `background-color` property, which fills the button with green. To add more effect to the button border, we also leverage `box-shadow`. Finally, we change the button text color to white, to improve the color contrast. The animation consists of one step, that is, at 50% of the animation, we apply a green background and add box shadow to the element. As an exercise, you may add more steps with extra colors for a smoother transition. The code is found below.
```{r, echo=FALSE, results='asis'}
css_code <- "@keyframes blink {
50% {
background-color: #16a520;
box-shadow: 0 0 10px 2px #16a520;
color: white;
}
}"
code_chunk_custom(css_code, "css")
```
We apply the animation with a delay of 1 second, each cycle lasts 1 second, repeated three times:
```{r, echo=FALSE, results='asis'}
css_code <- ".blink-green {
animation: blink 1s 1s 3 linear;
}"
code_chunk_custom(css_code, "css")
```
Notice the fourth parameter, which corresponds to the `animation-timing-function` [property](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function). It controls the animation speed curve. Passing it the `linear` value ensures a smoother transition than using the default `ease`. The whole code is shown below.
```{r, echo=FALSE, results='asis'}
code_chunk(OSUICode::get_example("css/animations/blink"), "r")
```
### Layout
As mentioned in section \@ref(web-intro-html), there exists `block` and `inline` tags. While block elements are displayed under each other, inline elements are added next to each other, as shown in Figure \@ref(fig:css-layout-intro).
```{r css-layout-intro, echo=FALSE, fig.cap='Inline and block elements.', out.width='25%', fig.align="center"}
knitr::include_graphics("images/beautify/css-layout-intro.png")
```
CSS allows deep customization of block elements like setting up the width, height and margin. Let's review them below.
#### Style blocks
As shown in Figure \@ref(fig:css-block-box), a block tag may be seen as a box. CSS allows you to set internal and external __margins__. Internal margins, also called __padding__, is the space between the block border and the content, in all directions. External margins also known as __margin__ are all spaces between blocks in all directions. A block is also defined by a __width__ and __height__, as well as optional minimal width and height. Width and height may be set either using absolute unites (px) or relative unites (%), but minimal values are always absolute. Minimal values are useful in case the end user resizes the windows.
The corresponding CSS properties are found below:
```{r, echo=FALSE, results='asis'}
css_code <- ".block {
width: 50%; /* % also work and will be relative size*/
height: 200px;
min-width: 200px;
}"
code_chunk_custom(css_code, "css")
```
Padding and margins are called CSS __super properties__. Indeed, setting `padding: 10px` guarantees having 10px in all directions. If you need custom values, you will have to specify the directions:
```{r, echo=FALSE, results='asis'}
css_code <- ".block {
padding: 10px;
margin-top: 10px;
margin-left: 5px;
}"