ducrot / twigcn-bundle
Beautiful, accessible UI components for Symfony & Twig. Inspired by shadcn/ui.
Requires
- php: >=8.2
- symfony/config: ^7.0
- symfony/dependency-injection: ^7.0
- symfony/http-kernel: ^7.0
- symfony/ux-icons: ^2.18
- symfony/ux-twig-component: ^2.18
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- symfony/framework-bundle: ^7.0
- symfony/twig-bundle: ^7.0
README
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.
Requirements
- PHP 8.2+
- Symfony 7.0+
- Node.js 22+ (for asset compilation)
- Tailwind CSS 4.0+
Installation
Step 1: Install the PHP Bundle
composer require ducrot/twigcn-bundle
The bundle registers automatically via Symfony Flex.
Step 2: Install the NPM Package
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.
Step 3: Configure Your CSS
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).
Step 4: Register Stimulus Controllers
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);
Alternative: Vite via pentatrion/vite-bundle
For projects that prefer a real bundler (recommended for Tailwind + TypeScript
- Stimulus), use
pentatrion/vite-bundletogether withvite-plugin-symfonyand@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-symfonyprovides 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.
Usage
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>
Available Components
Form
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 input
Layout
Accordion/AccordionItem- Collapsible sectionsBreadcrumb/BreadcrumbItem/BreadcrumbSeparator- Breadcrumb navigationCard- Container with border and shadowPagination/PaginationItem- Page navigationSidebar- Collapsible sidebar navigation (withSidebarHeader,SidebarContent,SidebarFooter,SidebarGroup,SidebarMenu,SidebarMenuItem,SidebarMenuButton)Table- Data tableTabs/TabsList/TabsTrigger/TabsContent- Tabbed content panels
Overlay
Command- Command palette (withCommandInput,CommandList,CommandGroup,CommandItem,CommandShortcut,CommandSeparator,CommandEmpty)Dialog- Modal dialog (use:closeOnBackdrop="false"for alert-dialog behavior)Drawer- Slide-out panel (withDrawerTrigger,DrawerContent,DrawerHeader,DrawerFooter,DrawerClose)DropdownMenu/DropdownMenuTrigger/DropdownMenuContent/DropdownMenuItem- Dropdown menuPopover/PopoverTrigger/PopoverContent- Floating content on triggerTooltip- Hover tooltip
Feedback
Alert- Alert messagesAvatar- User avatarBadge- Status badgesCarousel- Image/content carousel (withCarouselContent,CarouselItem,CarouselNext,CarouselPrevious)Empty- Empty-state placeholderItem- Generic content itemKbd- Keyboard key displayProgress- Progress barSkeleton- Loading placeholderSpinner- Loading spinnerThemeSwitcher- Dark/light mode toggleToast/Toaster(withToastTitle,ToastDescription,ToastClose) - Toast notifications
Naming Notes
A 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 |
Theming
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 */ }
Dark Mode
Toggle dark mode by adding/removing the dark class on the <html> element:
<twig:Twigcn:ThemeSwitcher />
Or manually:
document.documentElement.classList.toggle('dark');
Contributing & Issues
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.
License
MIT License