How to Use Sass and SCSS with Jekyll (Complete Guide)
A complete guide to using Sass and SCSS with Jekyll β directory structure, importing partials, variables, nesting, theming with custom properties, and compilation settings.
Jekyll has built-in Sass processing β no Node.js, no build tools, no webpack required. Write SCSS files, Jekyll compiles them to CSS automatically. This guide covers everything from basic setup to advanced patterns used in production Jekyll themes.
Sass vs SCSS
Sass has two syntaxes:
SCSS (Sassy CSS) β superset of CSS. Valid CSS is valid SCSS. Uses curly braces and semicolons. The most common syntax.
Sass (indented syntax) β uses indentation instead of braces, no semicolons. Older syntax, less commonly used.
Jekyll supports both. This guide uses SCSS (the .scss extension).
Jekyllβs Sass directory structure
Jekyll processes Sass files with this convention:
- Files in
_sass/starting with_are partials β they are imported by other files, never compiled directly - Files in
assets/css/(or anywhere outside_sass/) with.scssextension and front matter are entry points β Jekyll compiles these to CSS
_sass/ β partials live here
βββ _variables.scss
βββ _base.scss
βββ _nav.scss
βββ _cards.scss
βββ _post.scss
assets/
βββ css/
βββ main.scss β entry point β compiled to main.css
Creating the entry point
The entry point file needs front matter (even if empty) to tell Jekyll to process it:
/* assets/css/main.scss */
---
---
@import "variables";
@import "base";
@import "nav";
@import "cards";
@import "post";
The empty --- block is required. Without it, Jekyll copies the file as-is without processing.
Import partials without the leading _ or .scss extension β Sass resolves them automatically.
SCSS partials in _sass/
_variables.scss β design tokens
// _sass/_variables.scss
// Colours
$color-primary: #2563eb;
$color-primary-dark: #1d4ed8;
$color-text: #1a1a2e;
$color-muted: #6b7280;
$color-border: #e5e7eb;
$color-bg: #ffffff;
$color-bg-alt: #f9fafb;
// Typography
$font-sans: "Inter", system-ui, -apple-system, sans-serif;
$font-mono: "Fira Code", "Cascadia Code", monospace;
$font-size-base: 1rem;
$line-height-base: 1.6;
// Spacing
$spacing-xs: 0.25rem;
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
$spacing-lg: 1.5rem;
$spacing-xl: 2rem;
$spacing-2xl: 3rem;
// Layout
$container-max: 1200px;
$sidebar-width: 280px;
// Borders
$radius-sm: 4px;
$radius-md: 10px;
$radius-lg: 16px;
$radius-full: 9999px;
// Shadows
$shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
$shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
$shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
// Transitions
$transition-fast: 150ms ease;
$transition-base: 250ms ease;
$transition-slow: 400ms ease;
_base.scss β reset and global styles
// _sass/_base.scss
@import "variables";
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: $font-sans;
font-size: $font-size-base;
line-height: $line-height-base;
color: $color-text;
background: $color-bg;
}
img {
max-width: 100%;
height: auto;
display: block;
}
a {
color: $color-primary;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.3;
font-weight: 700;
margin-top: 0;
}
code {
font-family: $font-mono;
font-size: 0.875em;
background: $color-bg-alt;
padding: 0.15em 0.4em;
border-radius: $radius-sm;
}
SCSS features used in Jekyll themes
Variables
$color-primary: #2563eb;
.btn {
background: $color-primary;
&:hover {
background: darken($color-primary, 10%);
}
}
Nesting
.card {
border-radius: $radius-md;
overflow: hidden;
&__image {
width: 100%;
aspect-ratio: 16 / 9;
}
&__body {
padding: $spacing-lg;
}
&__title {
font-size: 1.125rem;
font-weight: 600;
margin: 0 0 $spacing-sm;
}
&--featured {
border: 2px solid $color-primary;
}
&:hover {
box-shadow: $shadow-md;
}
}
This BEM-style nesting (&__element, &--modifier) generates classes like .card__image, .card__body, .card--featured.
Mixins
// _sass/_mixins.scss
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin responsive($breakpoint) {
@if $breakpoint == mobile {
@media (max-width: 640px) { @content; }
} @else if $breakpoint == tablet {
@media (max-width: 1024px) { @content; }
} @else if $breakpoint == desktop {
@media (min-width: 1025px) { @content; }
}
}
// Usage
.nav {
@include flex-center;
@include responsive(mobile) {
flex-direction: column;
}
}
.card__title {
@include truncate;
}
Functions
// Convert px to rem
@function rem($px) {
@return ($px / 16) * 1rem;
}
.heading {
font-size: rem(24); // β 1.5rem
}
// Darken a colour by percentage
.btn:hover {
background: darken($color-primary, 8%);
}
// Generate a colour with opacity
.overlay {
background: rgba($color-text, 0.5);
}
Extends / placeholders
%visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.sr-only {
@extend %visually-hidden;
}
.skip-link:focus {
@extend %visually-hidden;
clip: auto;
width: auto;
height: auto;
}
Dark mode with Sass
// _sass/_variables.scss
// Light mode defaults (also used as CSS custom properties)
:root {
--color-bg: #{$color-bg};
--color-text: #{$color-text};
--color-border: #{$color-border};
--color-bg-alt: #{$color-bg-alt};
}
// Dark mode overrides
:root[data-theme="dark"],
.dark {
--color-bg: #0f172a;
--color-text: #f1f5f9;
--color-border: #1e293b;
--color-bg-alt: #1e293b;
}
// Use CSS custom properties throughout
body {
background: var(--color-bg);
color: var(--color-text);
}
.card {
border-color: var(--color-border);
background: var(--color-bg-alt);
}
This approach β setting CSS custom properties from Sass variables β gives you the best of both worlds: Sass for authoring, CSS variables for runtime dark mode toggling with JavaScript.
Sass compilation settings in _config.yml
# _config.yml
sass:
sass_dir: _sass # where partials live (default: _sass)
style: compressed # compressed | expanded | nested | compact
load_paths:
- _sass
- node_modules # if importing npm packages
style options:
compressedβ removes all whitespace, one-line output. Use for production.expandedβ each rule and property on its own line. Default in development.nestedβ rules nested to reflect the SCSS structure.compactβ one rule per line.
Most setups use compressed in production builds:
sass:
style: compressed
Importing npm Sass packages
If you install Sass libraries via npm, add the path to load_paths:
npm install sass-mq normalize.css
sass:
load_paths:
- _sass
- node_modules
// assets/css/main.scss
---
---
@import "normalize.css/normalize";
@import "sass-mq/mq";
@import "variables";
@import "base";
Organising a production-ready _sass/ directory
_sass/
βββ _variables.scss β design tokens
βββ _mixins.scss β reusable mixins
βββ _functions.scss β Sass functions
βββ _reset.scss β CSS reset/normalise
βββ _base.scss β global styles (body, a, h1-h6, img)
βββ _typography.scss β prose/content typography
β
βββ layout/
β βββ _container.scss
β βββ _grid.scss
β βββ _sections.scss
β
βββ components/
β βββ _nav.scss
β βββ _footer.scss
β βββ _cards.scss
β βββ _badges.scss
β βββ _buttons.scss
β βββ _forms.scss
β βββ _modals.scss
β
βββ pages/
β βββ _home.scss
β βββ _blog.scss
β βββ _theme-detail.scss
β βββ _authors.scss
β
βββ utilities/
βββ _helpers.scss β .sr-only, .clearfix, etc.
βββ _dark-mode.scss
/* assets/css/main.scss */
---
---
// Tokens and tools
@import "variables";
@import "mixins";
@import "functions";
// Base
@import "reset";
@import "base";
@import "typography";
// Layout
@import "layout/container";
@import "layout/grid";
@import "layout/sections";
// Components
@import "components/nav";
@import "components/footer";
@import "components/cards";
@import "components/badges";
@import "components/buttons";
@import "components/forms";
// Pages
@import "pages/home";
@import "pages/blog";
@import "pages/theme-detail";
@import "pages/authors";
// Utilities
@import "utilities/helpers";
@import "utilities/dark-mode";
Common mistakes
Missing front matter on the entry point: Without the --- block, Jekyll copies main.scss as a plain text file instead of compiling it. Always include the empty front matter.
Importing from the wrong path: Partials in _sass/ are imported without the leading _ or the directory path. If your partial is _sass/components/_nav.scss, import it as @import "components/nav".
Using @use instead of @import: Jekyllβs built-in Sass processor uses libsass which supports @import but has limited support for the newer @use syntax. Stick with @import for Jekyllβs native Sass processing. If you need @use, switch to a Node.js PostCSS pipeline.
Not compressing in production: Add sass: style: compressed to _config.yml or use a production build command to minimise CSS output.
Built-in Sass support is one of Jekyllβs most useful features. No Node.js, no build pipeline, no configuration β just write SCSS and Jekyll compiles it. For most Jekyll sites, the built-in processor is all you need.
Why Jekyllβs built-in Sass is enough for most sites
The built-in Sass processor handles the vast majority of use cases without any additional tooling. You get variables, nesting, mixins, functions, partials, and the ability to import npm packages via load_paths. For blogs, documentation sites, portfolios, and small business sites, this is everything you need.
The case for adding a Node.js build pipeline (PostCSS, Webpack, Vite) only becomes compelling when you need features the built-in processor cannot provide: Tailwind CSS utilities, PostCSS plugins like autoprefixer for vendor prefixes at scale, or the newer Sass @use/@forward module system. For most Jekyll sites, the complexity of adding a Node.js pipeline outweighs the benefits.
Start with Jekyllβs built-in Sass. Add a Node.js pipeline only when you have a specific, concrete reason that the built-in processor cannot address.
Writing maintainable SCSS for Jekyll themes
SCSS is a powerful tool, but it is also one that is easy to misuse. A few discipline practices keep Jekyll theme stylesheets maintainable over time.
Use CSS custom properties for runtime values, Sass variables for build-time values. Sass variables compile away β you cannot change them with JavaScript. CSS custom properties (--color-primary: blue) are accessible at runtime, making them essential for dark mode toggles, user preference systems, and theme switchers. The best practice is to define your design tokens as Sass variables and then assign them to CSS custom properties in :root:
$blue-600: #2563eb;
$blue-700: #1d4ed8;
:root {
--color-primary: #{$blue-600};
--color-primary-dark: #{$blue-700};
}
// Use the custom property everywhere (not the Sass variable)
.btn {
background: var(--color-primary);
&:hover { background: var(--color-primary-dark); }
}
This gives you the authoring convenience of Sass variables for defining the palette, and the runtime flexibility of CSS custom properties for applying them.
Avoid deep nesting. Nesting beyond three levels creates high-specificity selectors that are hard to override and indicate overly coupled HTML and CSS. A good rule: if you cannot read the compiled selector at a glance, the nesting is too deep. BEM naming with one level of nesting (&__element, &--modifier) produces clear, flat selectors without sacrificing the readability benefits of nesting.
Keep files focused. A _nav.scss file should contain only nav-related styles. When a file grows beyond 150β200 lines, consider splitting it. A _nav-desktop.scss and _nav-mobile.scss approach is sometimes cleaner than one large file with breakpoints scattered throughout.
Comment at the section level, not the line level. A comment explaining what a block of CSS achieves is valuable. A comment explaining what a single property does is usually not. Exception: non-obvious values like magic numbers, z-index values, and vendor-specific hacks benefit from inline comments explaining why they are there.
The @use and @forward system (Dart Sass)
Jekyllβs built-in Sass processor uses LibSass, which supports the older @import syntax. Dart Sass (the reference implementation) introduces @use and @forward as replacements that offer better encapsulation:
// @use namespaces modules automatically
@use "variables" as vars;
@use "mixins";
.btn {
background: vars.$color-primary;
@include mixins.flex-center;
}
@use prevents global namespace pollution by scoping module members under a namespace. @forward re-exports module members, useful for building a single entry point that exposes multiple partials.
If you want to use @use with Jekyll, you need to replace the built-in Sass processor with a Node.js-based pipeline using sass (the Dart Sass npm package) and PostCSS. The setup is more complex but enables the full modern Sass feature set.
For most Jekyll projects, the added complexity is not worth it. The @import system, while deprecated in Dart Sass, works perfectly well with Jekyllβs LibSass processor and will continue to do so for the foreseeable future.
Performance: how much CSS is too much?
The impact of your CSS file size on performance depends on how it is delivered. Jekyllβs built-in Sass with style: compressed produces minified CSS β no whitespace, comments stripped. This is important for production builds.
A rough guide: CSS under 50kb (uncompressed) has negligible performance impact on modern connections. Between 50kb and 150kb, the file size is noticeable on slower connections but unlikely to cause Lighthouse score problems. Above 150kb, you are probably including unused styles and should consider audit tooling.
For Jekyll themes, CSS bloat typically comes from including large third-party stylesheets (Bootstrap, Foundation, Bulma) without purging unused rules. If you import Bootstrap via npm and use only a fraction of its utilities, you are shipping tens of kilobytes of unused CSS.
The solution for third-party framework CSS is either to import only the specific component files you need:
// Import only Bootstrap's grid and buttons, not everything
@import "bootstrap/scss/grid";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/utilities/api";
Or to use a tool like PurgeCSS to remove unused rules from the production build. For Jekyll with a PostCSS pipeline, @fullhuman/postcss-purgecss scans your HTML and template files and removes any CSS rules that do not match used class names.
For most Jekyll sites that write their own SCSS rather than importing a large framework, CSS file size is not a meaningful concern. Write clear, modular stylesheets using the conventions in this guide, compress them in production, and focus on the variables and patterns that make your site easy to maintain and customise over time.
Explore the JekyllHub theme collection to see SCSS-driven Jekyll themes in action β the best themes demonstrate these conventions cleanly and serve as excellent references for your own SCSS architecture.
Debugging Sass compilation errors
When Jekyllβs Sass processor encounters an error, it stops the build and reports the file and line number. The error messages from LibSass are generally clear, but a few common patterns trip up beginners.
βUndefined variableβ β you referenced a variable ($color-primary) before declaring it or in a file that does not import _variables.scss. Fix: add @import "variables" at the top of the partial that uses the variable.
βFile to import not foundβ β the partial path in the @import statement does not match the actual filename. Check for typos and remember that partial filenames start with _ but are imported without it: @import "nav" imports _sass/_nav.scss.
βInvalid CSS afterβ β a syntax error in your SCSS. LibSassβs error messages point to a line but the actual error is sometimes a few lines earlier β an unclosed bracket or missing semicolon from a previous rule.
No error but CSS unchanged β Jekyll cached the previous build. Run bundle exec jekyll clean then bundle exec jekyll serve to force a fresh compilation.
For faster debugging cycles, use bundle exec jekyll serve with the --incremental flag during active CSS development β Jekyll only rebuilds changed files, dramatically reducing the time between saving a SCSS change and seeing it in the browser. When using --incremental, you may need to run a full clean build periodically to ensure all files are in sync.
Moving forward with Jekyll SCSS
Once you have the fundamentals working β imports, variables, mixins, and a sensible file structure β you have everything you need to write maintainable, scalable styles for any Jekyll project. The key habits to develop are keeping your SCSS partials small and focused, using variables for anything that appears more than once, and compiling locally so you catch errors before pushing to production.