@i.sangam/create-wp-theme
v1.0.2
Published
Scaffold a WordPress FSE theme with Vite + Tailwind v4 + Biome
Maintainers
Readme
create-wp-theme
A CLI scaffolding tool that generates production-ready WordPress Full Site Editing (FSE) themes with a modern frontend toolchain.
Combines Vite's speed, Tailwind CSS v4's simplicity, and Biome's all-in-one linting into a single zero-config starter.
Why this exists
Existing WordPress + Tailwind starters either lack FSE support, ship with outdated tooling, or require manual configuration to get hot reload working. This tool generates a theme that:
- Works with the WordPress Site Editor out of the box (theme.json, block templates, template parts)
- Uses Vite for instant hot module replacement — no page refreshes during CSS/JS development
- Automatically switches between dev server and production builds — zero manual file edits to toggle environments
- Produces deploy-ready folders and zips that contain only the files your server needs
- Uses class-based PHP with a clean singleton pattern instead of a monolithic
functions.php - Always installs the latest versions of every dependency
Quick start
With Bun (recommended):
bunx @i.sangam/create-wp-themeWith npm:
npx @i.sangam/create-wp-themeBoth work identically. Bun is recommended for faster installs and builds, but Node.js 18+ works fine too.
Interactive setup
The CLI walks you through each option one at a time. Only the theme name is required — everything else has smart defaults you can accept by pressing Enter.
If you leave a required field empty, the cursor stays in place and shows an inline error — no disruptive re-prompts.
URLs are auto-prefixed with https:// if you omit the protocol (e.g. typing example.com becomes https://example.com).
┌ create-wp-theme
│ WordPress FSE • Vite • Tailwind v4 • Biome
│
◆ Theme name *
│ e.g. Flavor starter, flavor starter
│ ❯ Flavor starter
│
◆ Theme slug
│ e.g. flavor_starter → folder: /flavor_starter, text-domain: flavor-starter
│ press Enter for flavor_starter
│ ❯
│
◆ Function prefix
│ e.g. flavor → Flavor_Theme, FLAVOR_THEME_DIR
│ press Enter for flavor
│ ❯
│
◆ Theme URL
│ e.g. example.com → https:// added automatically
│ ❯
│
◆ Author
│ e.g. John Doe, Starter Inc.
│ ❯
│
◆ Author URL
│ e.g. johndoe.com → https:// added automatically
│ ❯
│
◆ Description
│ e.g. A minimal starter theme for client projects
│ press Enter for Flavor starter — FSE WordPress theme
│ ❯
│
◆ Destination
│ e.g. ./wp-content/themes → creates ./flavor_starter/ inside it
│ press Enter for .
│ ❯
│
◇ Summary
│
│ Theme Flavor starter
│ Directory ./flavor_starter/
│ Slug flavor-starter
│ Prefix flavor → Flavor_Theme
│ Constants FLAVOR_THEME_DIR, FLAVOR_THEME_URI
│ Text Domain flavor-starter
│
◆ Create this theme?
│ Y/n ❯ Yes
│
◇ Scaffolding
│
│ ✓ Created directories
│ ✓ Written theme files
│ ✓ Dependencies installed
│ ✓ Initial build complete
│
└ Done! Theme created at ./flavor_starter/What each option does
| Option | Required | What it controls |
|---|---|---|
| Theme name | Yes | Display name in WordPress admin (Appearance → Themes) |
| Theme slug | No | Folder name, translation text domain, and CSS handle prefix. Default: theme name in snake_case |
| Function prefix | No | PHP class names and constants. E.g. flavor becomes Flavor_Theme, FLAVOR_THEME_DIR. Default: first word of slug |
| Theme URL | No | Link shown in theme details. https:// added automatically if omitted |
| Author | No | Author name shown in theme details |
| Author URL | No | Author link in theme details. Defaults to Theme URL if provided |
| Description | No | Short description shown in theme details. Default: {name} — FSE WordPress theme |
| Destination | No | Parent directory where the theme folder is created. Default: current directory (.) |
What you get
The generated theme follows WordPress FSE conventions with a clean separation between source files (for development) and production files (for deployment):
my_theme/
│
│ ── WordPress theme files (production) ──────────────────
│
├── functions.php Entry point — loads the class bootstrap
├── style.css Theme metadata (name, author, version, etc.)
├── theme.json FSE config: colors, fonts, layout, spacing
│
├── templates/ FSE block templates
│ ├── index.html Blog / archive listing
│ ├── page.html Static pages
│ ├── single.html Individual posts
│ └── 404.html Not found page
│
├── parts/ FSE block template parts
│ ├── header.html Site header (navigation, logo)
│ └── footer.html Site footer (copyright, links)
│
├── template-parts/ PHP template parts
│ └── common/ Shared partials across custom page templates
│
├── inc/ PHP classes (clean architecture)
│ ├── class-{prefix}-theme.php Singleton bootstrap, constants, dependency loader
│ ├── class-{prefix}-setup.php Theme supports, nav menus, custom logo, i18n
│ └── class-{prefix}-assets.php Asset enqueue with automatic HMR detection
│
├── assets/
│ ├── dist/ Compiled CSS + JS (generated by Vite)
│ └── fonts/ Self-hosted WOFF2 font files
│
│ ── Development files (not deployed) ────────────────────
│
├── src/
│ ├── main.css Tailwind CSS v4 source + custom styles
│ └── main.js JavaScript entry point (imports the CSS)
│
├── scripts/
│ └── bundle.js Production bundler (creates deploy folder or zip)
│
├── vite.config.js Vite config with Tailwind plugin + hot file management
├── biome.json Linter + formatter rules
├── package.json Scripts and devDependencies
└── .gitignore Ignores node_modules, dist, hot, deploy outputDevelopment workflow
1. Install dependencies
cd my_theme
bun install # or: npm install2. Start the dev server
bun run dev # or: npm run devWhat happens behind the scenes:
- Vite starts and compiles your Tailwind CSS + JS
- A
hotfile is created in the theme root - The PHP asset loader detects the
hotfile and loads CSS/JS fromlocalhost:5173instead ofassets/dist/ - Vite watches for file changes and pushes updates to the browser via WebSocket
This means:
- CSS changes (in
src/main.css) appear instantly — no page refresh needed - JS changes (in
src/main.js) are hot-swapped or trigger a minimal reload - PHP/HTML changes require a manual browser refresh (Vite only handles CSS/JS)
- No manual file edits are needed to switch between dev and production mode
3. Edit your files
| What you want to change | Where to edit |
|--------------------------------------|----------------------------------------------|
| Styles (Tailwind utilities + custom) | src/main.css |
| JavaScript | src/main.js |
| Theme colors, fonts, spacing, layout | theme.json |
| Block templates | templates/*.html |
| Block header / footer | parts/header.html, parts/footer.html |
| PHP template parts | template-parts/**/*.php |
| Theme supports, menus, logo | inc/class-{prefix}-setup.php |
| Asset loading behavior | inc/class-{prefix}-assets.php |
| Linting / formatting rules | biome.json |
| Vite build configuration | vite.config.js |
4. Stop the dev server
Press Ctrl+C. The hot file is automatically removed. The next time WordPress loads the page, it serves the compiled files from assets/dist/ instead. No configuration changes needed.
Production builds
Build + deploy folder
bun run build # or: npm run buildThis runs two steps automatically:
- Vite compiles Tailwind CSS + JS into
assets/dist/ - Bundle script copies only the production files into a
{theme_slug}/folder inside the theme root
The output folder is ready to copy directly to your server's wp-content/themes/ directory:
my_theme/ ← your development theme
├── my_theme/ ← copy this entire folder to your server
│ ├── assets/ (includes compiled dist/ and fonts/)
│ ├── inc/ (PHP classes)
│ ├── parts/ (block template parts)
│ ├── templates/ (block templates)
│ ├── template-parts/ (PHP partials)
│ ├── functions.php
│ ├── style.css
│ └── theme.jsonBuild + deploy zip
bun run build:zip # or: npm run build:zipSame as above, but packages everything into {theme_slug}.zip. This is useful for:
- Uploading via WordPress admin (Appearance → Themes → Add New → Upload Theme)
- Distributing the theme to clients or other developers
- Storing versioned releases
What's included vs excluded in production builds
| Included (deployed to server) | Excluded (development only) |
|-------------------------------------------------------------|-----------------------------------------------|
| assets/ (compiled CSS/JS in dist/ + fonts/) | node_modules/ (dependencies) |
| inc/ (PHP classes) | src/ (Tailwind source CSS, JS source) |
| parts/ (FSE block template parts) | scripts/ (build tooling) |
| templates/ (FSE block templates) | package.json, bun.lock |
| template-parts/ (PHP partials) | vite.config.js, biome.json |
| functions.php, style.css, theme.json | .gitignore, README.md, hot |
All commands
| Command | What it does |
|----------------------|-------------------------------------------------------------------|
| bun run dev | Start Vite dev server with hot module replacement |
| bun run build | Compile CSS/JS and create a deploy-ready theme folder |
| bun run build:zip | Compile CSS/JS and create a deploy-ready theme zip |
| bun run lint | Check JS/CSS files for errors and issues with Biome |
| bun run lint:fix | Check and automatically fix issues with Biome |
| bun run format | Format JS/CSS files with Biome |
All commands also work with npm run instead of bun run.
How auto HMR detection works
The theme uses a hot file mechanism (inspired by Laravel Vite) to automatically detect whether to load assets from the Vite dev server or from the compiled dist/ folder:
Developer runs `bun run dev`
│
▼
Vite starts → creates empty `hot` file in theme root
│
▼
WordPress loads a page → PHP checks: does `hot` file exist?
│
┌───┴───┐
YES NO
│ │
▼ ▼
Enqueue from Enqueue from
localhost:5173 assets/dist/
(live dev server) (compiled files)
│
▼
Developer stops Vite (Ctrl+C)
│
▼
Vite plugin removes `hot` file automatically
│
▼
Next page load → `hot` file gone → serves from dist/This is handled by two files working together:
vite.config.js— A custom Vite plugin creates thehotfile when the dev server starts and removes it when the server stopsclass-{prefix}-assets.php— Theis_vite_dev()method checks for thehotfile and routes asset loading accordingly
You never need to manually edit either file to switch between development and production modes.
PHP architecture
The theme avoids the common WordPress anti-pattern of a monolithic functions.php with hundreds of lines of unrelated code. Instead, it uses a class-based singleton pattern with separated concerns:
functions.php
└── requires and boots {Prefix}_Theme (singleton)
│
├── define_constants()
│ ├── {PREFIX}_THEME_VERSION → Theme version from style.css
│ ├── {PREFIX}_THEME_DIR → Absolute server path to theme root
│ └── {PREFIX}_THEME_URI → URL to theme root
│
├── load_dependencies()
│ ├── class-{prefix}-setup.php
│ └── class-{prefix}-assets.php
│
└── register_hooks()
├── {Prefix}_Setup::init()
│ └── after_setup_theme → supports, menus, logo, i18n
│
└── {Prefix}_Assets::init()
├── wp_enqueue_scripts → auto HMR or production assets
└── script_loader_tag → adds type="module" for ViteAdding new functionality
To add a new feature (e.g. custom post types, widgets, REST API endpoints):
- Create a new class file:
inc/class-{prefix}-{feature}.php - Follow the same pattern — a
final classwith apublic static function init()that registers WordPress hooks - Require it in
class-{prefix}-theme.php'sload_dependencies()method - Call
{Prefix}_{Feature}::init()in theregister_hooks()method
Example:
// inc/class-my-custom-posts.php
final class My_Custom_Posts {
public static function init(): void {
add_action( 'init', [ __CLASS__, 'register' ] );
}
public static function register(): void {
register_post_type( 'portfolio', [ /* ... */ ] );
}
}Tailwind CSS v4
The theme uses Tailwind CSS v4's CSS-first configuration. There is no tailwind.config.js file. All customization happens directly in src/main.css using the @theme directive:
@import "tailwindcss";
@theme {
--font-heading: "Inter", sans-serif;
--font-body: "Noto Sans JP", sans-serif;
--color-primary: #0073aa;
--color-secondary: #23282d;
}
/* Custom component styles */
.btn-primary {
@apply bg-primary text-white px-6 py-3 rounded-lg font-semibold
hover:bg-primary/90 transition-colors;
}Tailwind automatically scans all .php, .html, and .js files in the theme directory for class names and generates only the CSS you actually use.
For more details, see the Tailwind CSS v4 documentation.
Adding custom fonts
WordPress FSE themes load fonts through theme.json, not CSS @font-face rules. This ensures fonts work in both the front end and the block editor.
Step 1: Add font files
Place your .woff2 font files in assets/fonts/:
assets/fonts/
├── Inter-Regular.woff2
├── Inter-Bold.woff2
└── NotoSansJP-Variable.woff2Step 2: Register in theme.json
Add a fontFace entry under settings.typography.fontFamilies:
{
"fontFamily": "\"Inter\", sans-serif",
"name": "Inter",
"slug": "inter",
"fontFace": [
{
"fontFamily": "Inter",
"fontWeight": "400",
"fontStyle": "normal",
"fontDisplay": "swap",
"src": ["file:./assets/fonts/Inter-Regular.woff2"]
},
{
"fontFamily": "Inter",
"fontWeight": "700",
"fontStyle": "normal",
"fontDisplay": "swap",
"src": ["file:./assets/fonts/Inter-Bold.woff2"]
}
]
}For variable fonts, use a weight range:
{
"fontWeight": "100 900",
"src": ["file:./assets/fonts/NotoSansJP-Variable.woff2"]
}Step 3: Make it available in Tailwind
Add the font to @theme in src/main.css:
@theme {
--font-inter: "Inter", sans-serif;
}Step 4: Use it
In templates: class="font-inter"
In theme.json styles:
"typography": {
"fontFamily": "var(--wp--preset--font-family--inter)"
}Adding custom page templates
The block editor handles most pages, but some pages need PHP logic — dynamic data, API calls, complex layouts with sliders or carousels, or integration with third-party services.
Step 1: Create the PHP template
Add a PHP file at the theme root following WordPress's template hierarchy:
| File name | When WordPress uses it |
|---|---|
| front-page.php | Static front page (Settings → Reading → "A static page") |
| page-{slug}.php | Specific page by slug (e.g. page-about.php for /about/) |
| single-{post-type}.php | Custom post type single view |
| archive-{post-type}.php | Custom post type archive |
PHP templates at the root take precedence over block templates in templates/.
Step 2: Add template parts
Create partials in template-parts/ to keep your template organized:
template-parts/
├── common/ Shared across pages (header, footer)
└── frontpage/ Front page specific
├── hero.php
├── features.php
└── testimonials.phpUse get_template_part() to include them:
<?php get_template_part( 'template-parts/frontpage/hero' ); ?>Step 3: Add page-specific CSS/JS (if needed)
If the page needs its own styles or scripts beyond main.css/main.js:
- Create new source files (e.g.
src/frontpage/frontpage.js+src/frontpage/frontpage.css) - Add the entry point to
vite.config.js:
rollupOptions: {
input: {
main: resolve(__dirname, 'src/main.js'),
frontpage: resolve(__dirname, 'src/frontpage/frontpage.js'), // new
},
}- Enqueue conditionally in
class-{prefix}-assets.php:
if ( is_front_page() ) {
wp_enqueue_style( '{slug}-frontpage', $dist_uri . '/frontpage.css', [], ... );
wp_enqueue_script( '{slug}-frontpage', $dist_uri . '/frontpage.js', [], ... );
}Requirements
| Requirement | Minimum version | Notes | |---|---|---| | Bun or Node.js | Bun 1+ / Node 18+ | Bun recommended for speed; both work | | PHP | 8.0+ | Required by WordPress and the generated theme | | WordPress | 6.4+ | Required for full FSE support (theme.json v3, block templates) |
Inspiration
This project was inspired by _tw and TailPress — both great tools for WordPress + Tailwind development. create-wp-theme builds on that foundation with a focus on WordPress FSE, modern tooling, and a streamlined developer experience.
Features at a glance
| Feature | | |---|---| | WordPress FSE (theme.json, block templates) | Yes | | Tailwind CSS v4 (CSS-first config) | Yes | | Vite (fast builds) | Yes | | Auto HMR (no manual dev/prod switching) | Yes | | Biome (linter + formatter) | Yes | | Deploy-ready builds (folder + zip) | Yes | | Class-based PHP architecture | Yes | | Interactive CLI scaffolding | Yes | | Always latest package versions | Yes | | Works with Bun and npm | Yes |
Acknowledgments
This tool was crafted collaboratively with Claude Code by Anthropic — from architecture decisions and code generation to documentation. Every pattern in the scaffolding (auto HMR detection, class-based PHP, production bundling, the interactive CLI itself) was iteratively designed and refined through that partnership to be as performant and efficient as possible.
License
MIT
