Eleventy Theme Setup and Customization
Eleventy doesn't have an official theme marketplace — the main source is community starters and official partner starter templates. Unlike Hugo or Jekyll, "themes" in Eleventy are full-fledged projects with dependencies that are forked and customized, not connected as a package. This provides more freedom and requires understanding the structure of someone else's code.
Popular starters
| Starter | Features |
|---|---|
eleventy-base-blog (official) |
Minimal blog, Nunjucks |
11ty-webc-starter |
WebC components, modern stack |
eleventy-excellent |
Full set: RSS, sitemap, OG, dark mode |
eleventy-duo |
Portfolio + blog |
slinkity |
React/Vue components + Vite |
Cloning a starter:
# Using degit (without git history)
npx degit 11ty/eleventy-base-blog mysite
cd mysite
npm install
npm start
Typical starter structure
eleventy-base-blog/
├── eleventy.config.js # Config — entry point for customization
├── src/
│ ├── _includes/
│ │ └── layouts/
│ │ ├── base.njk
│ │ └── post.njk
│ ├── _data/
│ │ └── metadata.json # Replace first thing
│ ├── blog/
│ ├── css/ # Styles — edit directly
│ └── index.njk
├── package.json
└── .eleventy.js # Sometimes both config files exist
Initial setup: metadata and config
// src/_data/metadata.json
{
"title": "Site Title",
"url": "https://example.com",
"language": "en",
"description": "Description for SEO",
"author": {
"name": "Author Name",
"email": "[email protected]",
"url": "https://example.com/about/"
}
}
// eleventy.config.js — add custom logic
module.exports = function(eleventyConfig) {
// Base starter settings (don't touch)
// ...
// Your additions:
eleventyConfig.addFilter("enDate", function(dateObj) {
return new Intl.DateTimeFormat("en-US", {
day: "numeric",
month: "long",
year: "numeric"
}).format(new Date(dateObj));
});
eleventyConfig.addFilter("limit", function(arr, limit) {
return arr.slice(0, limit);
});
// Additional passthrough
eleventyConfig.addPassthroughCopy("src/uploads");
};
Nunjucks template customization
The principle is the same as Hugo: find the needed template, edit it directly (starter is a copy, not a package).
Adding navigation to base.njk:
{# src/_includes/layouts/base.njk #}
<!DOCTYPE html>
<html lang="{{ metadata.language }}">
<head>
...
</html>
<body>
<header class="site-header">
<div class="container">
<a href="/" class="logo">
<img src="/assets/images/logo.svg" alt="{{ metadata.title }}">
</a>
<nav class="main-nav" aria-label="Main navigation">
{% for item in navigation %}
<a href="{{ item.url }}"
{% if page.url == item.url %}aria-current="page"{% endif %}
class="nav-link">
{{ item.label }}
</a>
{% endfor %}
</nav>
</div>
</header>
...
// src/_data/navigation.json
[
{ "label": "Home", "url": "/" },
{ "label": "Services", "url": "/services/" },
{ "label": "Blog", "url": "/blog/" },
{ "label": "Contact", "url": "/contact/" }
]
CSS customization
Starters use different approaches to styles: vanilla CSS custom properties, SASS, Tailwind. Typical customization through CSS variables:
/* src/css/custom.css — add after starter imports */
:root {
/* Override theme variables */
--color-primary: #2563eb;
--color-primary-dark: #1d4ed8;
--color-text: #1e293b;
--color-bg: #ffffff;
--color-bg-muted: #f8fafc;
--font-sans: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
--radius: 8px;
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--container-max: 1280px;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
--color-text: #e2e8f0;
--color-bg: #0f172a;
--color-bg-muted: #1e293b;
}
}
If starter uses Tailwind, customize through tailwind.config.js:
module.exports = {
content: ["./src/**/*.{njk,md,js,html}"],
theme: {
extend: {
colors: {
primary: { 500: '#2563eb', 600: '#1d4ed8' },
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
},
},
};
Adding new sections and components
{# src/_includes/components/features.njk #}
{% set features = features or [] %}
<section class="features">
<div class="container">
{% if featuresTitle %}
<h2 class="section-title">{{ featuresTitle }}</h2>
{% endif %}
<div class="features-grid">
{% for feature in features %}
<div class="feature-card">
{% if feature.icon %}
<div class="feature-icon">{{ feature.icon | safe }}</div>
{% endif %}
<h3>{{ feature.name }}</h3>
<p>{{ feature.description }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
Usage on page:
---
layout: layouts/base.njk
title: About Us
featuresTitle: Our Advantages
features:
- name: "Experience"
icon: "<svg>...</svg>"
description: "10 years of development"
- name: "Team"
icon: "<svg>...</svg>"
description: "25 specialists"
---
Adding RSS and Sitemap
If starter doesn't include:
npm install @11ty/eleventy-plugin-rss
// eleventy.config.js
const pluginRss = require("@11ty/eleventy-plugin-rss");
eleventyConfig.addPlugin(pluginRss);
{# src/feed.njk #}
---
permalink: /feed.xml
eleventyExcludeFromCollections: true
---
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>{{ metadata.title }}</title>
<link href="{{ metadata.url }}/feed.xml" rel="self"/>
<updated>{{ collections.blog | getNewestCollectionItemDate | dateToRfc3339 }}</updated>
{% for post in collections.blog | reverse | limit(10) %}
<entry>
<title>{{ post.data.title }}</title>
<link href="{{ metadata.url }}{{ post.url }}"/>
<updated>{{ post.date | dateToRfc3339 }}</updated>
<content type="html">{{ post.templateContent | htmlToAbsoluteUrls(metadata.url) }}</content>
</entry>
{% endfor %}
</feed>
Timeline
Initial starter setup (metadata, navigation, colors, fonts) — 1–2 days. Deep template customization, adding components, custom tag page, RSS — 3–6 days.







