ducrot/twigcn-bundle
TwigcnBundle brings shadcn/ui-inspired, accessible UI components to Symfony & Twig. Install the PHP bundle plus the @ducrot/twigcn-ui NPM package and compile Tailwind CSS to use ready-made, themeable components in your templates.
Read-only mirror. This repository is a subtree split of
ducrot/twigcnand exists so Composer/Packagist can ship the bundle. Issues and pull requests are intentionally disabled here — please open them in the main repository.
Beautiful, accessible UI components for Symfony & Twig. Inspired by shadcn/ui, built for Symfony.
composer require ducrot/twigcn-bundle
The bundle registers automatically via Symfony Flex.
npm install @ducrot/twigcn-ui
If your project uses AssetMapper (the Symfony 7.x webapp default), also
expose the package to the importmap so it can be imported from JavaScript:
php bin/console importmap:require @ducrot/twigcn-ui
Webpack Encore projects pick up node_modules automatically and do not need
this step.
Tailwind processes a source file into a static stylesheet. Keep the source
out of assets/ (so AssetMapper does not also serve it raw) — for example
tailwind.css at the project root:
@import "tailwindcss";
@import "@ducrot/twigcn-ui/styles";
/* IMPORTANT: Scan bundle templates for Tailwind classes */
@source "../vendor/ducrot/twigcn-bundle/templates";
/* Scan your own templates */
@source "./templates/**/*.html.twig";
/* Optional: Override theme variables */
:root {
--primary: #6b5fc3;
--primary-foreground: #ffffff;
--radius: 0.5rem;
}
Then build with the Tailwind CLI (Symfony does not bundle CSS itself):
npx @tailwindcss/cli -i tailwind.css -o assets/styles/app.css --watch
The compiled output at assets/styles/app.css is what AssetMapper or Encore
serves. Re-run the build whenever templates or styles change (or use
--watch during development).
Option A: Automatic (Symfony UX)
Create or update assets/controllers.json:
{
"controllers": {
"@ducrot/twigcn-ui": {
"accordion": { "enabled": true },
"carousel": { "enabled": true },
"combobox": { "enabled": true },
"command": { "enabled": true },
"custom-select": { "enabled": true },
"dialog": { "enabled": true },
"drawer": { "enabled": true },
"drawer-trigger": { "enabled": true },
"popover": { "enabled": true },
"slider": { "enabled": true },
"tabs": { "enabled": true },
"theme": { "enabled": true },
"toaster": { "enabled": true },
"tooltip": { "enabled": true }
}
}
}
Option B: Manual Registration
In a Symfony project that uses StimulusBundle (the default in the webapp
recipe), register the controllers on the existing Stimulus application from
assets/bootstrap.js (or assets/stimulus_bootstrap.js) — do not start a
second one:
import { startStimulusApp } from '@symfony/stimulus-bundle';
import { registerControllers } from '@ducrot/twigcn-ui';
const app = startStimulusApp();
registerControllers(app);
For non-Symfony or fully custom Stimulus setups (e.g. Webpack Encore without StimulusBundle):
import { Application } from '@hotwired/stimulus';
import { registerControllers } from '@ducrot/twigcn-ui';
const app = Application.start();
registerControllers(app);
pentatrion/vite-bundleFor projects that prefer a real bundler (recommended for Tailwind + TypeScript
pentatrion/vite-bundle together with
vite-plugin-symfony and @tailwindcss/vite. This is the setup
the demo app in this repository uses. Vite replaces both the
Tailwind CLI build (Tailwind 4 runs as a Vite plugin) and the StimulusBundle
controller registration (vite-plugin-symfony provides its own Stimulus
integration).Install (replaces Steps 2–4 above):
composer require pentatrion/vite-bundle ducrot/twigcn-bundle
npm install @ducrot/twigcn-ui
npm install --save-dev vite vite-plugin-symfony @tailwindcss/vite tailwindcss
vite.config.ts:
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';
import symfonyPlugin from 'vite-plugin-symfony';
import { resolve } from 'path';
export default defineConfig({
plugins: [
symfonyPlugin({ stimulus: true }),
tailwindcss(),
],
publicDir: false,
build: {
manifest: true,
outDir: 'public/build',
rollupOptions: {
input: { app: resolve(__dirname, 'assets/app.ts') },
},
},
});
assets/app.ts:
import './app.css';
import { Application } from '@hotwired/stimulus';
import { registerControllers } from '@ducrot/twigcn-ui';
const app = Application.start();
registerControllers(app);
assets/app.css:
@import "tailwindcss";
@import "@ducrot/twigcn-ui/styles";
/* Scan bundle templates *and* PHP component classes for Tailwind classes —
* variant strings live in PHP, so the @source must include `src` too. */
@source "../vendor/ducrot/twigcn-bundle/templates";
@source "../vendor/ducrot/twigcn-bundle/src";
@source "../templates";
In your base Twig template:
{% block stylesheets %}
{{ vite_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ vite_entry_script_tags('app') }}
{% endblock %}
Then npm run dev for HMR or npm run build for production. The bundle's
config/packages/pentatrion_vite.yaml
in the demo shows the minimal Symfony-side configuration.
Components are namespaced under Twigcn: in Twig (Symfony UX TwigComponent prefixes third-party bundle components with their bundle namespace):
{# Button #}
<twig:Twigcn:Button variant="primary" size="lg">Click me</twig:Twigcn:Button>
{# Button as link #}
<twig:Twigcn:Button as="a" href="/dashboard" variant="outline">
Go to Dashboard
</twig:Twigcn:Button>
{# Dialog (uses the native HTML <dialog> element) #}
<twig:Twigcn:Button onclick="document.getElementById('confirm-dialog').showModal()">
Open Dialog
</twig:Twigcn:Button>
<twig:Twigcn:Dialog id="confirm-dialog">
<header>
<h2 class="text-lg font-semibold">Confirm Action</h2>
<p class="text-muted-foreground">Are you sure you want to proceed?</p>
</header>
<footer>
<twig:Twigcn:Button variant="outline" onclick="document.getElementById('confirm-dialog').close()">
Cancel
</twig:Twigcn:Button>
<twig:Twigcn:Button variant="destructive" onclick="document.getElementById('confirm-dialog').close()">
Confirm
</twig:Twigcn:Button>
</footer>
</twig:Twigcn:Dialog>
{# Drawer (open via the dedicated DrawerTrigger) #}
<twig:Twigcn:DrawerTrigger for="settings-drawer" class="btn-outline">
Open Settings
</twig:Twigcn:DrawerTrigger>
<twig:Twigcn:Drawer id="settings-drawer" side="right">
<twig:Twigcn:DrawerContent>
<twig:Twigcn:DrawerHeader>
<h3 class="text-lg font-semibold">Settings</h3>
</twig:Twigcn:DrawerHeader>
<div class="p-4">Drawer content goes here.</div>
<twig:Twigcn:DrawerFooter>
<twig:Twigcn:DrawerClose class="btn-outline">Close</twig:Twigcn:DrawerClose>
</twig:Twigcn:DrawerFooter>
</twig:Twigcn:DrawerContent>
</twig:Twigcn:Drawer>
{# Tabs #}
<twig:Twigcn:Tabs defaultValue="account">
<twig:Twigcn:TabsList>
<twig:Twigcn:TabsTrigger value="account">Account</twig:Twigcn:TabsTrigger>
<twig:Twigcn:TabsTrigger value="password">Password</twig:Twigcn:TabsTrigger>
</twig:Twigcn:TabsList>
<twig:Twigcn:TabsContent value="account">
<p>Manage your account settings here.</p>
</twig:Twigcn:TabsContent>
<twig:Twigcn:TabsContent value="password">
<p>Change your password here.</p>
</twig:Twigcn:TabsContent>
</twig:Twigcn:Tabs>
{# Accordion #}
<twig:Twigcn:Accordion type="single" collapsible>
<twig:Twigcn:AccordionItem value="item-1" title="Is it accessible?">
Yes. It adheres to the WAI-ARIA design pattern.
</twig:Twigcn:AccordionItem>
<twig:Twigcn:AccordionItem value="item-2" title="Is it styled?">
Yes. It comes with default styles using Tailwind CSS.
</twig:Twigcn:AccordionItem>
</twig:Twigcn:Accordion>
{# Form elements #}
<twig:Twigcn:Field>
<twig:Twigcn:Label for="email">Email</twig:Twigcn:Label>
<twig:Twigcn:Input type="email" id="email" placeholder="you@example.com" />
</twig:Twigcn:Field>
{# Alerts #}
<twig:Twigcn:Alert variant="destructive">
<strong>Error!</strong> Something went wrong.
</twig:Twigcn:Alert>
Button - Clickable button with variantsButtonGroup - Group of related buttonsCheckbox - Checkbox inputChoiceCard - Selectable card-style optionCombobox - Autocomplete input with suggestionsCustomSelect - Enhanced select with searchField - Form field wrapperForm - Form containerInput - Text input fieldInputGroup - Input with prefix/suffix slotsLabel - Form labelRadio / RadioGroup - Radio buttonsSelect - Native select dropdownSlider - Range sliderSwitch - Toggle switchTextarea - Multi-line text inputAccordion / AccordionItem - Collapsible sectionsBreadcrumb / BreadcrumbItem / BreadcrumbSeparator - Breadcrumb navigationCard - Container with border and shadowPagination / PaginationItem - Page navigationSidebar - Collapsible sidebar navigation (with SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarMenu, SidebarMenuItem, SidebarMenuButton)Table - Data tableTabs / TabsList / TabsTrigger / TabsContent - Tabbed content panelsCommand - Command palette (with CommandInput, CommandList, CommandGroup, CommandItem, CommandShortcut, CommandSeparator, CommandEmpty)Dialog - Modal dialog (use :closeOnBackdrop="false" for alert-dialog behavior)Drawer - Slide-out panel (with DrawerTrigger, DrawerContent, DrawerHeader, DrawerFooter, DrawerClose)DropdownMenu / DropdownMenuTrigger / DropdownMenuContent / DropdownMenuItem - Dropdown menuPopover / PopoverTrigger / PopoverContent - Floating content on triggerTooltip - Hover tooltipAlert - Alert messagesAvatar - User avatarBadge - Status badgesCarousel - Image/content carousel (with CarouselContent, CarouselItem, CarouselNext, CarouselPrevious)Empty - Empty-state placeholderItem - Generic content itemKbd - Keyboard key displayProgress - Progress barSkeleton - Loading placeholderSpinner - Loading spinnerThemeSwitcher - Dark/light mode toggleToast / Toaster (with ToastTitle, ToastDescription, ToastClose) - Toast notificationsA few PHP class names differ from their Twig component tag because the tag name is reserved or already taken in PHP:
| Twig tag | PHP class |
|---|---|
<twig:Twigcn:Empty> |
EmptyState |
<twig:Twigcn:Switch> |
SwitchComponent |
<twig:Twigcn:Field> |
FieldForm |
Components use CSS custom properties for theming. Override these in your CSS:
:root {
/* Colors */
--background: #ffffff;
--foreground: #333333;
--primary: #6b5fc3;
--primary-foreground: #ffffff;
--secondary: #e7e7ea;
--secondary-foreground: #4e4d58;
--muted: #ececef;
--muted-foreground: #6c6b75;
--accent: #d6d9f0;
--accent-foreground: #433669;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #dbdadf;
--input: #dbdadf;
--ring: #6b5fc3;
/* Radius */
--radius: 0.375rem;
}
/* Dark mode */
.dark {
--background: #171717;
--foreground: #e5e5e5;
/* ... other dark mode overrides */
}
Toggle dark mode by adding/removing the dark class on the <html> element:
<twig:Twigcn:ThemeSwitcher />
Or manually:
document.documentElement.classList.toggle('dark');
Development happens in ducrot/twigcn.
Open issues at https://github.com/ducrot/twigcn/issues and pull requests
against the same repo. Release notes are tracked in the monorepo's
CHANGELOG.
MIT License
How can I help you explore Laravel packages today?