Theme Development
CSS & Assets

CSS & Assets

How to style your theme with CSS and manage static assets.

Asset Structure

my-theme/
├── assets/
│   ├── css/
│   │   ├── main.css
│   │   ├── components/
│   │   │   ├── header.css
│   │   │   ├── footer.css
│   │   │   └── cards.css
│   │   └── pages/
│   │       ├── home.css
│   │       └── post.css
│   ├── js/
│   │   └── main.js
│   ├── images/
│   │   └── logo.svg
│   └── fonts/
│       └── custom-font.woff2
└── theme.json

Registering Assets

In theme.json, declare your CSS and JavaScript files:

{
  "name": "My Theme",
  "version": "1.0.0",
  "assets": {
    "css": [
      "css/main.css"
    ],
    "js": [
      "js/main.js"
    ]
  }
}

Multiple files:

{
  "assets": {
    "css": [
      "css/normalize.css",
      "css/main.css",
      "css/responsive.css"
    ],
    "js": [
      "js/vendor/lightbox.min.js",
      "js/main.js"
    ]
  }
}

Asset URLs

In templates, reference assets using the theme path:

<link rel="stylesheet" href="/themes/my-theme/assets/css/main.css">
<script src="/themes/my-theme/assets/js/main.js"></script>
<img src="/themes/my-theme/assets/images/logo.svg" alt="Logo">

Or use the theme variable:

<link rel="stylesheet" href="{{ themePath }}/assets/css/main.css">

CSS Best Practices

Use CSS Variables

Define a consistent design system:

:root {
  /* Colors */
  --color-primary: #007bff;
  --color-secondary: #6c757d;
  --color-success: #28a745;
  --color-danger: #dc3545;
  --color-warning: #ffc107;
  --color-info: #17a2b8;
  
  /* Text */
  --color-text: #333;
  --color-text-muted: #6c757d;
  --color-text-light: #fff;
  
  /* Backgrounds */
  --color-bg: #fff;
  --color-bg-light: #f8f9fa;
  --color-bg-dark: #343a40;
  
  /* Borders */
  --color-border: #dee2e6;
  --border-radius: 4px;
  --border-radius-lg: 8px;
  
  /* Typography */
  --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-size-base: 16px;
  --line-height-base: 1.6;
  
  /* Spacing */
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 3rem;
  
  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
  --shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
  
  /* Transitions */
  --transition-fast: 0.15s ease;
  --transition-normal: 0.3s ease;
}

Dark Mode Support

@media (prefers-color-scheme: dark) {
  :root {
    --color-text: #e9ecef;
    --color-text-muted: #adb5bd;
    --color-bg: #1a1a1a;
    --color-bg-light: #2d2d2d;
    --color-border: #495057;
  }
}
 
/* Or with a class toggle */
.dark-mode {
  --color-text: #e9ecef;
  --color-bg: #1a1a1a;
}

Responsive Design

/* Mobile-first approach */
.container {
  width: 100%;
  padding: 0 var(--spacing-md);
}
 
/* Tablet */
@media (min-width: 768px) {
  .container {
    max-width: 720px;
    margin: 0 auto;
  }
}
 
/* Desktop */
@media (min-width: 1024px) {
  .container {
    max-width: 960px;
  }
}
 
/* Large screens */
@media (min-width: 1280px) {
  .container {
    max-width: 1200px;
  }
}

Component Styles

Header

.header {
  background: var(--color-bg);
  border-bottom: 1px solid var(--color-border);
  padding: var(--spacing-md) 0;
  position: sticky;
  top: 0;
  z-index: 100;
}
 
.header__inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
 
.header__logo {
  font-size: 1.5rem;
  font-weight: bold;
  text-decoration: none;
  color: var(--color-text);
}
 
.header__nav ul {
  display: flex;
  gap: var(--spacing-lg);
  list-style: none;
  margin: 0;
  padding: 0;
}
 
.header__nav a {
  color: var(--color-text);
  text-decoration: none;
  transition: color var(--transition-fast);
}
 
.header__nav a:hover {
  color: var(--color-primary);
}

Post Cards

.post-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--spacing-lg);
}
 
.post-card {
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius-lg);
  overflow: hidden;
  transition: box-shadow var(--transition-normal);
}
 
.post-card:hover {
  box-shadow: var(--shadow-lg);
}
 
.post-card__image {
  aspect-ratio: 16/9;
  overflow: hidden;
}
 
.post-card__image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--transition-normal);
}
 
.post-card:hover .post-card__image img {
  transform: scale(1.05);
}
 
.post-card__content {
  padding: var(--spacing-md);
}
 
.post-card__category {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  color: var(--color-primary);
  margin-bottom: var(--spacing-sm);
}
 
.post-card__title {
  font-size: 1.25rem;
  margin: 0 0 var(--spacing-sm) 0;
}
 
.post-card__title a {
  color: var(--color-text);
  text-decoration: none;
}
 
.post-card__excerpt {
  color: var(--color-text-muted);
  font-size: 0.9rem;
  margin-bottom: var(--spacing-md);
}
 
.post-card__meta {
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

Buttons

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem 1rem;
  font-size: 1rem;
  font-weight: 500;
  text-decoration: none;
  border: none;
  border-radius: var(--border-radius);
  cursor: pointer;
  transition: all var(--transition-fast);
}
 
.btn-primary {
  background: var(--color-primary);
  color: var(--color-text-light);
}
 
.btn-primary:hover {
  background: color-mix(in srgb, var(--color-primary) 85%, black);
}
 
.btn-secondary {
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text);
}
 
.btn-secondary:hover {
  background: var(--color-bg-light);
}
 
.btn-lg {
  padding: 0.75rem 1.5rem;
  font-size: 1.125rem;
}
 
.btn-sm {
  padding: 0.25rem 0.75rem;
  font-size: 0.875rem;
}

JavaScript

Main JS File

// assets/js/main.js
 
document.addEventListener('DOMContentLoaded', () => {
  initMobileMenu();
  initSmoothScroll();
  initLazyLoad();
});
 
function initMobileMenu() {
  const toggle = document.querySelector('.mobile-menu-toggle');
  const nav = document.querySelector('.header__nav');
  
  if (toggle && nav) {
    toggle.addEventListener('click', () => {
      nav.classList.toggle('is-open');
      toggle.classList.toggle('is-active');
    });
  }
}
 
function initSmoothScroll() {
  document.querySelectorAll('a[href^="#"]').forEach(anchor => {
    anchor.addEventListener('click', (e) => {
      const target = document.querySelector(anchor.getAttribute('href'));
      if (target) {
        e.preventDefault();
        target.scrollIntoView({ behavior: 'smooth' });
      }
    });
  });
}
 
function initLazyLoad() {
  const images = document.querySelectorAll('img[data-src]');
  
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.removeAttribute('data-src');
        observer.unobserve(img);
      }
    });
  });
  
  images.forEach(img => observer.observe(img));
}

Loading JavaScript Conditionally

In your layout, you can load JS at the end of body:

<!DOCTYPE html>
<html>
<head>
  {{{ include('partials/head') }}}
</head>
<body>
  {{{ include('partials/header') }}}
  
  <main>
    {{{ content }}}
  </main>
  
  {{{ include('partials/footer') }}}
  
  <script src="{{ themePath }}/assets/js/main.js"></script>
  
  {% if (page === 'gallery') { %}
    <script src="{{ themePath }}/assets/js/lightbox.js"></script>
  {% } %}
</body>
</html>

Custom Fonts

Using Google Fonts

In your head partial:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

In CSS:

:root {
  --font-family: 'Inter', sans-serif;
}
 
body {
  font-family: var(--font-family);
}

Self-hosted Fonts

@font-face {
  font-family: 'CustomFont';
  src: url('/themes/my-theme/assets/fonts/custom-font.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
 
@font-face {
  font-family: 'CustomFont';
  src: url('/themes/my-theme/assets/fonts/custom-font-bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

Build Tools (Optional)

For advanced themes, you can use build tools:

Using Sass

my-theme/
├── src/
│   └── scss/
│       ├── main.scss
│       ├── _variables.scss
│       └── _components.scss
├── assets/
│   └── css/
│       └── main.css (compiled)
└── package.json
{
  "scripts": {
    "build:css": "sass src/scss/main.scss assets/css/main.css --style=compressed",
    "watch:css": "sass src/scss/main.scss assets/css/main.css --watch"
  }
}

Using PostCSS

{
  "scripts": {
    "build:css": "postcss src/css/main.css -o assets/css/main.css"
  },
  "devDependencies": {
    "autoprefixer": "^10.4.0",
    "cssnano": "^6.0.0",
    "postcss": "^8.4.0",
    "postcss-cli": "^11.0.0"
  }
}