Note
The default Tailwind CSS WordPress theme for LiveCanvas
A modern WordPress theme for LiveCanvas with multi-engine template support (Twig, Blade, Latte, Handlebars) built on Timber and Tailwind CSS.
- PHP >= 8.2
- Node.js >= 24.9.0
- Composer
- pnpm
Clone the repository to your wp-content/themes directory:
git clone https://github.com/livecanvas-team/picowind picowind
cd picowind# Install PHP dependencies
composer install
# Install Node.js dependencies
pnpm install# Start Vite dev server with HMR
pnpm run devPicowind supports four template engines:
You can use the PHP functions to render templates and strings with different engines.
Picowind\render($paths, $context, $engine, $print, $silent)- Render template filePicowind\render_string($string, $context, $engine, $print)- Render template string
When rendering templates, Picowind will automatically detect the engine based on the file extension:
.twig- Twig.blade.php,.php, or no extension - Blade.latte- Latte.hbs,.handlebars- Handlebars
Picowind\render() supports mixed-engine fallback arrays out of the box. Leave $engine as null (default), pass multiple template candidates (even from different engines), and Picowind will try them in order until one renders successfully.
For extension-based fallback candidates, the default engine order is:
- Twig (
.twig) - Blade (
.blade.php) - Latte (
.latte) - Handlebars (
.hbs,.handlebars)
This is the order generated by Picowind\template_fallbacks() and used by the default theme template loaders.
Picowind\render([
'components/hero.twig',
'components/hero.blade.php',
'components/hero.latte',
'components/hero.hbs',
], ['title' => 'Hello']);You can also use the .? extension placeholder with an explicit engine to swap engines without changing template paths:
Picowind\render('components/hero.?', ['title' => 'Hello'], 'twig');
Picowind\render('components/hero.?', ['title' => 'Hello'], 'blade');
Picowind\render('components/hero.?', ['title' => 'Hello'], 'latte');
Picowind\render('components/hero.?', ['title' => 'Hello'], 'handlebars');Picowind\render('components/card.twig', ['title' => 'Hello']);
Picowind\render_string('<div>{{ title }}</div>', ['title' => 'Hello'], 'twig');Picowind\render('components/button', ['text' => 'Click me']);
Picowind\render_string('<div>{{ $text }}</div>', ['text' => 'Hello'], 'blade');Picowind\render('components/header.latte', ['title' => 'Welcome']);
Picowind\render_string('<div>{$title}</div>', ['title' => 'Hello'], 'latte');Picowind\render('components/card.hbs', ['title' => 'Welcome']);
Picowind\render_string('<div>{{title}}</div>', ['title' => 'Hello'], 'handlebars');Twig includes Timber helpers like get_post, get_posts, function, fn, and translation functions.
Picowind now exposes the same Timber callable map to Blade, Latte, and Handlebars:
- Latte
- Direct function names where Latte allows it:
{get_post(123)} function/fnare available ascall:{call('wp_head')}- Universal dispatcher:
{timber('get_post', 123)} _-prefixed helpers are supported directly (for example{__('Text', 'picowind')}), and Picowind also supports shorthand like{_n('%s star', '%s stars', 2, 'picowind')}.
- Direct function names where Latte allows it:
- Blade
- Callable variables for Timber functions:
{{ $get_post(123) }} function/fncall style:{{ $function('wp_head') }}and{{ $fn('wp_head') }}- Universal object access:
{{ $timber->get_post(123) }}
- Callable variables for Timber functions:
- Handlebars
- Direct helper names where Handlebars allows it:
{{get_post 123}} - Universal dispatcher:
{{timber "get_post" 123}} - Raw HTML helper output should use triple braces when appropriate, for example
{{{function "wp_head"}}}.
- Direct helper names where Handlebars allows it:
Picowind lets templates call other engines directly, so you can compose Twig, Blade, Latte, and Handlebars in the same page.
- Twig -> Blade/Latte/Handlebars
{% blade 'components/button.blade.php' with {'text': 'Click me'} %}
{% latte 'components/card.latte' with {'title': post.title} %}
{% handlebars 'components/card.hbs' with {'title': post.title} %}
{{ blade('components/button.blade.php', {'text': 'Click me'}) }}
{{ latte('components/card.latte', {'title': post.title}) }}
{{ handlebars('components/card.hbs', {'title': post.title}) }}- Blade -> Twig/Latte/Handlebars
@twig('components/card.twig', ['title' => $title])
@latte('components/card.latte', ['title' => $title])
@handlebars('components/card.hbs', ['title' => $title])- Latte -> Twig/Blade/Handlebars
{twig 'components/card.twig', ['title' => $title]}
{blade 'components/button.blade.php', ['text' => $title]}
{handlebars 'components/card.hbs', ['title' => $title]}
{twig('components/card.twig', ['title' => $title])}
{blade('components/button.blade.php', ['text' => $title])}
{handlebars('components/card.hbs', ['title' => $title])}- Handlebars -> Twig/Blade/Latte/Handlebars
You can also use shortcodes to render templates and strings directly in WordPress content.
[twig template="components/card.twig"]
[twig]<div>{{ site.name }}</div>[/twig]
[blade template="components/button"]
[blade]<div>{{ $user->name }}</div>[/blade]
[latte template="components/header.latte"]
[latte]<div>{$post->title}</div>[/latte]
picowind/
├── blocks/ # Block editor blocks
├── child-theme/ # Bundled child themes
├── public/ # Static assets
├── src/ # Theme functionality (PHP) (source)
├── resources/ # Admin and frontend assets (source)
└── views/ # Theme templates (Twig, Blade, Latte, Handlebars, etc.)
Templates are loaded from multiple directories in order of priority:
- Child theme
views/,blocks/,components/directory (if applicable) - Child theme root directory (if applicable)
- Parent theme
views/,blocks/,components/directory - Parent theme root directory
Picowind provides several PHP helper functions for common tasks:
Picowind\context()- Get global WordPress contextPicowind\omni_icon($name, $attributes)- Render SVG icons via Omni Icon plugin (supports Iconify, local uploads, and bundled icons)
Picowind uses PHP attributes for auto-discovery of services and hooks.
You can simply annotate your classes and methods with #[Service] and #[Hook] attributes to register them automatically.
For example:
// src/Path/To/MyFeature.php
use Picowind\Core\Discovery\Attributes\Service;
use Picowind\Core\Discovery\Attributes\Hook;
#[Service]
class MyFeature
{
#[Hook('init', type: 'action')]
public function my_powerful_feature(): void
{
// Your code here
}
}This will automatically registered and hooked into WordPress without manual intervention. It is equivalent to:
// src/Path/To/MyFeature.php
class MyFeature
{
public function __construct()
{
add_action('init', [$this, 'my_powerful_feature']);
}
public function my_powerful_feature(): void
{
// Your code here
}
}// functions.php
require_once __DIR__ . '/src/Path/To/MyFeature.php';
$my_feature = new MyFeature();