How to style your own website with Quarto
CSS and SCSS · SORTEE Code Club · Part 2
Cecilia Baldoni
Where we left off
Same .qmd files. Only one new file: styles.scss.
Today
Agenda:
- How is a web page built?
- CSS: how do I tell the browser what things look like?
- SCSS: how do I avoid repeating myself?
- How does Quarto use SCSS?
Slides ~15 min · live coding ~40–45 min · Q&A
Follow along
- Download: github.com/SORTEE/CodeClub → green Code → Download ZIP → unzip
- Copy the folder
20260519_WebsiteQuarto and open it in RStudio or VS Code
- Terminal: run
quarto preview
- Edit and save: the browser updates live
Used Git before? git clone https://github.com/SORTEE/CodeClub.git
Full steps and troubleshooting: handout.md in the repo.
Part 1 · How is a web page built?
What does a web page need
HTML (HyperText Markup Language) says what is on the page.
CSS (Cascading Style Sheets) says what it looks like.
Each piece of content sits between tags: <h2>…</h2> is a heading.
The browser reads both and draws the page.
Wait a minute…
Do we need to learn (S)CSS AND HTML??
Quarto for the rescue!
Quarto is awesome, because it takes care of everything:
::: {.callout-note}
## Next Session
:::
Quarto turns it into HTML like this (simplified):
<div class="callout callout-note">
No need to worry about HTML!
Part 2 · CSS: how do I tell the browser what things look like?
A CSS rule, piece by piece
h2 is the selector: which elements? All level-2 headings.
color is the property: what do we change? The text colour.
#0B5400 is the value: change it to what? SORTEE green. (A hex colour)
{ } hold the instructions
: joins property and value
; ends each one.
Selectors: pointing at elements
h2 { … }
.callout-note { … }
.navbar .nav-link { … }
.btn:hover { … }
h2: a tag name, no symbol. Picks every <h2> heading.
.callout-note: a class, marked by the .. Picks every element with that label.
.navbar .nav-link: two classes, joined by a space = “inside”. Picks menu links inside the navbar.
.btn:hover: a class plus a colon = a state. Picks a button, but only while the mouse is over it.
A class is just a label. Quarto has a few already (e.g. callout-note), but we can invent our own!
When two rules disagree: a tie
One heading, two rules that both set its colour. Which one wins?
h2 { color: #0B5400; } /* written first */
h2 { color: navy; } /* written second */
The browser gives every selector a score, by counting its parts. Each tag (h2) = 1 point.
h2 (first) |
one tag = 1 |
1 |
h2 (second) |
one tag = 1 |
1 |
A tie: the rule written later wins. The heading is navy.
Fun fact: that’s where the ‘Cascading’ in CascadingStyleSheet comes in!
When two rules disagree: class beats tag
Now the heading has a class, and a class is worth more: each class (.highlight) = 10 points. Which rule wins this time?
<h2 class="highlight">Who We Are</h2>
.highlight { color: #0B5400; } /* written first */
h2 { color: navy; } /* written second */
.highlight |
one class = 10 |
10 |
h2 |
one tag = 1 |
1 |
Higher score wins, so the heading is green, even though h2 comes later. Order only matters on a tie.
DevTools: see the CSS of any page
- Right-click anything on the page → Inspect.
- Left panel: the HTML. Look for
class="…" to find the labels.
- Right panel (Styles): every rule that applies. Rules that lost are
crossed out.
- You can edit values there to try things out. Nothing is saved.
Try it on this box. Which rule lost?
Part 3 · SCSS: how do I avoid repeating myself?
The problem with plain CSS
.navbar { background: #0B5400; }
h2 { color: #0B5400; }
.btn { border-color: #0B5400; }
New brand colour?
You’ll need to find and change every copy.
What is SCSS?
- SCSS (Sassy CSS) is CSS plus a few extras: variables and nesting (and many more).
- Any CSS is already valid SCSS.
- Browsers only understand CSS, so SCSS has to be translated into CSS with Sass.
- Quarto does that for you!
styles.scss → (Quarto → Sass → CSS) → browser
How does Quarto do that?
Every SCSS file is split into sections with a marker comment:
/*-- scss:defaults --*/ for variables,
/*-- scss:rules --*/ for rules.
Each snippet from here on starts with its marker. (Why: Part 4.)
What is a variable?
A variable is a name that stores a value. You already use them in R:
sortee_green <- "#0B5400"
In SCSS, a variable starts with $:
/*-- scss:defaults --*/
$sortee-green: #0B5400;
$primary: $sortee-green;
Change the value once, and everything that uses the name updates.
Nesting: rules inside rules
CSS: two separate rules
.btn-outline-primary { border-width: 2px; }
.btn-outline-primary:hover { color: #ffffff; }
SCSS: one rule inside the other
/*-- scss:rules --*/
.btn-outline-primary {
border-width: 2px;
&:hover { color: #ffffff; }
}
& means “the selector I’m inside”, so &:hover = .btn-outline-primary:hover.
Nesting, things inside things
CSS: the parent name is repeated every time
.navbar { background: #0B5400; }
.navbar .nav-link { color: white; }
.navbar .nav-link:hover { text-decoration: underline; }
SCSS: write the parent once
/*-- scss:rules --*/
.navbar {
background: #0B5400;
.nav-link {
color: white;
&:hover { text-decoration: underline; }
}
}
A rule with no & inside another means “an element inside it”: .nav-link becomes .navbar .nav-link.
Nesting, variants of the same thing
CSS: every variant repeats the full name
.callout { border-left-width: 6px; }
.callout.callout-note { border-left-color: #0B5400; }
.callout-note .callout-title { font-weight: 600; }
SCSS: & also glues a class onto the parent
/*-- scss:rules --*/
.callout {
border-left-width: 6px;
&.callout-note {
border-left-color: #0B5400;
.callout-title { font-weight: 600; }
}
}
&.callout-note has no space: it’s the same element with both classes. .callout-title with a space is an element inside.
Part 4 · How does Quarto use SCSS?
Quarto comes with ready-made themes
Render a website and it already looks styled. Why?
- Quarto has a collection of pre-made themes:
flatly, cosmo, darkly, …
- A theme is a set of values for variables like
$primary (the main colour).
Our Part 1 site already uses one: theme: flatly.
Today: we write on top of flatly
theme: [flatly, styles.scss]
flatly is applied first.
styles.scss is ours, applied after it.
Wherever both say something, ours wins. Anything we don’t mention stays as flatly made it.
You never edit flatly. You only add a small file after it.
Two sections: defaults and rules
Quarto reads your file in two pieces, and puts each in a different place:
/*-- scss:defaults --*/
$primary: #0B5400; // variables
/*-- scss:rules --*/
h2 { color: $primary; } // CSS rules
/*-- scss:defaults --*/ |
variables ($primary: …) |
before the theme is built |
/*-- scss:rules --*/ |
CSS rules (h2 { … }, nesting) |
after the theme’s CSS, so yours come later |
Why it matters
- A variable under
rules is set too late: the theme has already used its own value. Nothing happens, no error.
- A rule under
defaults lands before the theme’s CSS, so the theme can override it.
- No markers at all: Quarto stops with an error.
You can repeat a marker as often as you like. Quarto collects all the defaults pieces together, and all the rules pieces together.
Our file: styles.scss
In _quarto.yml: theme: [flatly, styles.scss]
/*-- scss:defaults --*/
$sortee-green: #0B5400;
$primary: $sortee-green;
/*-- scss:rules --*/
h2 { border-bottom: 3px solid $primary; }
This is the file we build today, one step at a time: variables first, then rules, each under its own marker.
Variables we’ll set today
Colour
$sortee-green
$primary
$secondary
$body-bg
$body-color
$link-color
$link-hover-color
Typography
$font-family-sans-serif
$headings-font-family
$headings-font-weight
$h1h2h3-font-weight
$font-size-root
$line-height-base
Quarto components
$navbar-bg
$navbar-fg
$navbar-hl
$footer-bg
$footer-fg
$callout-color-note
$code-block-bg
$code-block-border-left
Shape
$border-radius
$btn-border-radius
Go further
What we skipped: position, flexbox and grid, media queries, partials and @use, dark mode.