How to style your own website with Quarto

CSS and SCSS · SORTEE Code Club · Part 2

Cecilia Baldoni

Housekeeping

Where we left off

The Part 1 website with the default flatly theme: dark blue-grey navbar and title banner, teal links, Lato font.

Part 1: flatly, out of the box

The same website after today's styles.scss: SORTEE green navbar and banner, Roboto Slab headings with green underlines, a green-tinted note callout.

Today: it looks like SORTEE

Same .qmd files. Only one new file: styles.scss.

Today

Agenda:

  1. How is a web page built?
  2. CSS: how do I tell the browser what things look like?
  3. SCSS: how do I avoid repeating myself?
  4. How does Quarto use SCSS?

Slides ~15 min · live coding ~40–45 min · Q&A

Follow along

  1. Download: github.com/SORTEE/CodeClub → green Code → Download ZIP → unzip
  2. Copy the folder 20260519_WebsiteQuarto and open it in RStudio or VS Code
  3. Terminal: run quarto preview
  4. 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.

<h2>Who We Are</h2>

CSS (Cascading Style Sheets) says what it looks like.

h2 { color: #0B5400; }



Each piece of content sits between tags: <h2>…</h2> is a heading.

The browser reads both and draws the page.

Wait a minute…

Prince John from Disney's Robin Hood throwing a tantrum, shouting no.



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 { color: #0B5400; }
  1. h2 is the selector: which elements? All level-2 headings.
  1. color is the property: what do we change? The text colour.
  1. #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 { … }
  1. h2: a tag name, no symbol. Picks every <h2> heading.
  2. .callout-note: a class, marked by the .. Picks every element with that label.
  3. .navbar .nav-link: two classes, joined by a space = “inside”. Picks menu links inside the navbar.
  4. .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>Who We Are</h2>
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.

Rule Parts Score
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 */
Rule Parts Score
.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

  1. Right-click anything on the page → Inspect.
  2. Left panel: the HTML. Look for class="…" to find the labels.
  3. Right panel (Styles): every rule that applies. Rules that lost are crossed out.
  4. 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?

Prince John from Disney's Robin Hood throwing a tantrum, shouting no.

  • 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
Marker What goes in Where Quarto puts it
/*-- 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.