🎨 A modern Symfony UI bundle with 70+ reusable Twig components styled with TailwindCSS. Build beautiful, consistent interfaces faster with pre-built form controls, navigation, tables, and more.
composer require cisse/ui-bundle
Add to your config/bundles.php:
<?php
return [
// ...
Cisse\Bundle\UiBundle\UiBundleBundle::class => ['all' => true],
];
# Required for TailwindCSS integration
composer require symfonycasts/tailwind-bundle
# Recommended for advanced class merging
composer require gehrisandro/tailwind-merge-php
Create your main CSS file with the required setup:
@import "tailwindcss";
@source "../../vendor/cisse/ui-bundle"; /* ⚠️ MANDATORY */
/* Your custom theme variables... */
| Component | Description |
|---|---|
<twig:Ui:input> |
Smart input with automatic type detection |
<twig:Ui:input:text> <twig:Ui:input:textarea> |
Text inputs and textareas |
<twig:Ui:input:checkbox> <twig:Ui:input:date> |
Checkboxes and date pickers |
<twig:Ui:input:email> <twig:Ui:input:url> |
Email and URL inputs with validation |
<twig:Ui:input:phone> <twig:Ui:input:money> |
Phone and money inputs |
<twig:Ui:input:number> <twig:Ui:input:quantity> |
Number and quantity inputs |
<twig:Ui:input:otp> |
One-time password input |
<twig:Ui:input:search> |
Search input with icon |
<twig:Ui:label> |
Form labels with proper styling |
<twig:Ui:select> |
Styled select dropdowns |
<twig:Ui:combobox> |
Searchable select with autocomplete |
<twig:Ui:switch> |
Toggle switches |
<twig:Ui:slider> <twig:Ui:range-slider> |
Slider and range slider inputs |
<twig:Ui:rating> |
Star rating input |
<twig:Ui:color-picker> |
Color selection input |
<twig:Ui:icon-picker> |
Icon selection input |
<twig:Ui:file-upload> |
File upload with drag and drop |
<twig:Ui:tags-input> |
Tags/chips input |
<twig:Ui:form> |
Form container with validation |
<twig:Ui:form-section> |
Form section with header and content |
<twig:Ui:form-actions> |
Form action buttons container |
<twig:Ui:input-group> <twig:Ui:input-wrapper> |
Input grouping and wrapping |
<twig:Ui:input-addon> |
Input addons (prefix/suffix) |
| Component | Description |
|---|---|
<twig:Ui:button> |
Buttons with multiple variants (primary, secondary, error) |
<twig:Ui:button-group> |
Group related buttons together with shared styling |
<twig:Ui:badge> |
Status badges with colors, sizes, and variants |
<twig:Ui:alert> |
Alert notifications with icons and dismiss functionality |
<twig:Ui:card> |
Card layouts with header, content, and footer sections |
<twig:Ui:collapsible-card> |
Cards with expandable/collapsible content |
<twig:Ui:modal> |
Modal dialogs with backdrop |
<twig:Ui:confirm-dialog> |
Confirmation dialogs for destructive actions |
<twig:Ui:slide-over> |
Slide-out panels for details |
<twig:Ui:dropdown> |
Dropdown menus with positioning options |
<twig:Ui:popover> |
Contextual popovers with rich content |
<twig:Ui:tooltip> |
Contextual tooltips |
<twig:Ui:divider> |
Visual content separators |
<twig:Ui:avatar> |
User avatars with images or initials |
<twig:Ui:kbd> |
Keyboard key display for shortcuts |
<twig:Ui:code-block> |
Code display with optional copy button |
<twig:Ui:command-palette> |
Quick navigation and actions palette (⌘K) |
| Component | Description |
|---|---|
<twig:Ui:menu> <twig:Ui:menu:item> |
Navigation menus with sub-menus |
<twig:Ui:tabs> <twig:Ui:tabs:item> |
Tabbed navigation with keyboard support |
<twig:Ui:filter-tabs> |
Filter tabs for list filtering |
<twig:Ui:breadcrumb> |
Breadcrumb navigation |
<twig:Ui:pagination> |
Pagination controls |
<twig:Ui:stepper> |
Step-by-step progress indicator |
<twig:Ui:timeline> |
Vertical timeline for events |
| Component | Description |
|---|---|
<twig:Ui:table> <twig:Ui:data-table> |
Responsive tables with sorting |
<twig:Ui:table:selectable> |
Tables with row selection |
<twig:Ui:thead> <twig:Ui:tbody> <twig:Ui:tfoot> |
Table sections |
<twig:Ui:tr> <twig:Ui:th> <twig:Ui:td> |
Table rows and cells (tr supports expandable) |
<twig:Ui:datalist> |
Definition lists for key-value pairs |
<twig:Ui:responsive-list> |
Responsive list with desktop/mobile views |
<twig:Ui:mobile-list> |
Mobile-optimized list display |
<twig:Ui:stat> <twig:Ui:stat:item> |
Statistics display with trends |
<twig:Ui:boolean> |
Boolean value display with icons |
| Component | Description |
|---|---|
<twig:Ui:accordion> |
Collapsible content sections |
<twig:Ui:empty-state> |
Placeholder for empty data states |
<twig:Ui:skeleton> |
Loading placeholders (text, card, table, list) |
<twig:Ui:loading-spinner> |
Loading spinner indicator |
<twig:Ui:progress> |
Progress bars |
<twig:Ui:toast> |
Toast notifications |
<twig:Ui:notification-list> |
Notification list display |
<twig:Ui:dark-mode-toggle> |
Dark/light mode switcher |
<twig:Ui:container> |
Responsive container wrapper |
{# Button colors #}
<twig:Ui:button color="primary">Primary</twig:Ui:button>
<twig:Ui:button color="secondary">Secondary</twig:Ui:button>
<twig:Ui:button color="success">Success</twig:Ui:button>
<twig:Ui:button color="warning">Warning</twig:Ui:button>
<twig:Ui:button color="danger">Danger</twig:Ui:button>
<twig:Ui:button color="info">Info</twig:Ui:button>
<twig:Ui:button color="neutral">Neutral</twig:Ui:button>
<twig:Ui:button color="white">White</twig:Ui:button>
<twig:Ui:button color="black">Black</twig:Ui:button>
{# Button variants #}
<twig:Ui:button variant="solid" color="primary">Solid</twig:Ui:button>
<twig:Ui:button variant="outline" color="primary">Outline</twig:Ui:button>
<twig:Ui:button variant="ghost" color="primary">Ghost</twig:Ui:button>
<twig:Ui:button variant="soft" color="primary">Soft</twig:Ui:button>
{# White/Black buttons swap in dark mode #}
<twig:Ui:button color="white">White in light → Black in dark</twig:Ui:button>
<twig:Ui:button color="black">Black in light → White in dark</twig:Ui:button>
{# Link buttons #}
<twig:Ui:button href="/dashboard" color="primary">Go to Dashboard</twig:Ui:button>
{# Custom styling #}
<twig:Ui:button color="primary" class="w-full mt-4">Full Width Submit</twig:Ui:button>
{# Basic badges with different colors #}
<twig:Ui:badge>Default</twig:Ui:badge>
<twig:Ui:badge color="primary">Primary</twig:Ui:badge>
<twig:Ui:badge color="success">Success</twig:Ui:badge>
<twig:Ui:badge color="error">Error</twig:Ui:badge>
<twig:Ui:badge color="warning">Warning</twig:Ui:badge>
<twig:Ui:badge color="info">Info</twig:Ui:badge>
{# Different sizes #}
<twig:Ui:badge size="sm">Small</twig:Ui:badge>
<twig:Ui:badge>Default</twig:Ui:badge>
<twig:Ui:badge size="lg">Large</twig:Ui:badge>
{# Different variants #}
<twig:Ui:badge variant="solid" color="primary">Solid</twig:Ui:badge>
<twig:Ui:badge variant="outline" color="primary">Outline</twig:Ui:badge>
<twig:Ui:badge variant="soft" color="primary">Soft</twig:Ui:badge>
{# Badge with dot indicator #}
<twig:Ui:badge dot color="success">Online</twig:Ui:badge>
<twig:Ui:badge dot color="error">Offline</twig:Ui:badge>
{# Clickable badges (links) #}
<twig:Ui:badge href="/admin/users" color="info">5 Users</twig:Ui:badge>
<twig:Ui:badge href="/notifications" color="error">3 Alerts</twig:Ui:badge>
{# Status badges for lists #}
<div class="space-y-2">
<div class="flex items-center justify-between">
<span>Database Connection</span>
<twig:Ui:badge color="success" dot>Connected</twig:Ui:badge>
</div>
<div class="flex items-center justify-between">
<span>Background Jobs</span>
<twig:Ui:badge color="warning" dot>2 Pending</twig:Ui:badge>
</div>
<div class="flex items-center justify-between">
<span>Error Logs</span>
<twig:Ui:badge color="error" variant="outline">5 Errors</twig:Ui:badge>
</div>
</div>
{# Boolean prop shortcuts (backward compatibility) #}
<twig:Ui:badge primary>Primary</twig:Ui:badge>
<twig:Ui:badge success small>Success Small</twig:Ui:badge>
<twig:Ui:badge error outline>Error Outline</twig:Ui:badge>
{# Basic alerts with different types #}
<twig:Ui:alert color="success" title="Success!">
Your changes have been saved successfully.
</twig:Ui:alert>
<twig:Ui:alert color="error" title="Error occurred">
There was a problem processing your request. Please try again.
</twig:Ui:alert>
<twig:Ui:alert color="warning" title="Warning">
Your session will expire in 5 minutes. Please save your work.
</twig:Ui:alert>
<twig:Ui:alert color="info" title="Information">
New features are now available. Check out the changelog.
</twig:Ui:alert>
{# Alert without icon #}
<twig:Ui:alert color="primary" title="Notice" icon="false">
This is a simple alert without an icon.
</twig:Ui:alert>
{# Alert with only content (no title) #}
<twig:Ui:alert color="success">
Quick success message without a title.
</twig:Ui:alert>
{# Dismissible alerts #}
<twig:Ui:alert color="info" title="Dismissible Alert" dismissible>
You can close this alert by clicking the X button.
</twig:Ui:alert>
{# Outline variant #}
<twig:Ui:alert variant="outline" color="warning" title="Outline Warning">
This alert has an outline style instead of filled background.
</twig:Ui:alert>
{# Rich content alert #}
<twig:Ui:alert color="success" title="Payment Confirmation">
<p>Payment for <strong>{{ user.name }}</strong> has been processed successfully.</p>
<div class="mt-3">
<twig:Ui:button color="success" size="sm" href="/receipt">
View Receipt
</twig:Ui:button>
<twig:Ui:button variant="outline" color="success" size="sm" href="/dashboard">
Back to Dashboard
</twig:Ui:button>
</div>
</twig:Ui:alert>
{# Form validation alerts #}
{% if form.vars.errors|length > 0 %}
<twig:Ui:alert color="error" title="Form Validation Errors" dismissible>
<ul class="list-disc list-inside space-y-1">
{% for error in form.vars.errors %}
<li>{{ error.message }}</li>
{% endfor %}
</ul>
</twig:Ui:alert>
{% endif %}
{# Boolean shortcuts (backward compatibility) #}
<twig:Ui:alert success title="Success">Success alert using boolean prop</twig:Ui:alert>
<twig:Ui:alert error outline dismissible title="Error">Error outline alert</twig:Ui:alert>
// Listen for alert dismiss events
document.addEventListener('alert:dismissed', (event) => {
console.log('Alert dismissed:', event.detail)
// Optional: Track analytics
gtag('event', 'alert_dismissed', {
'alert_color': event.detail.color,
'alert_variant': event.detail.variant
})
})
// Programmatically dismiss alerts
const alertController = application.getControllerForElementAndIdentifier(
document.querySelector('[data-controller="cisse--ui-bundle--alert"]'),
'cisse--ui-bundle--alert'
)
alertController.hide()
<twig:Ui:card divide>
<twig:block name="title">🎯 Project Overview</twig:block>
<twig:block name="description">Track your project progress and metrics</twig:block>
<twig:block name="content">
<div class="space-y-4">
<p>✅ 12 tasks completed</p>
<p>⏳ 3 tasks in progress</p>
</div>
</twig:block>
<twig:block name="actions">
<twig:Ui:button primary>View Details</twig:Ui:button>
<twig:Ui:button secondary>Edit Project</twig:Ui:button>
</twig:block>
</twig:Ui:card>
{# Symfony Form Integration #}
<twig:Ui:form>
<div class="space-y-4">
<div>
<twig:Ui:label>{{ form_label(form.email) }}</twig:Ui:label>
<twig:Ui:input form="{{ form.email }}" />
</div>
<div>
<twig:Ui:label>{{ form_label(form.message) }}</twig:Ui:label>
<twig:Ui:input:textarea form="{{ form.message }}" rows="4" />
</div>
<twig:Ui:button type="submit" primary class="w-full">
Send Message
</twig:Ui:button>
</div>
</twig:Ui:form>
{# Standalone Form Elements #}
<div class="space-y-4">
<twig:Ui:input type="email"
name="email"
placeholder="your@email.com"
required />
<twig:Ui:select name="role">
<option value="">Choose a role...</option>
<option value="admin">Administrator</option>
<option value="user">User</option>
</twig:Ui:select>
</div>
{# Basic tabs with different colors #}
<twig:Ui:tabs current="{{ current_filter }}">
<twig:Ui:tabs:item href="{{ path('dashboard_index') }}"
current="{{ current_filter == 'all' }}">
All Items
</twig:Ui:tabs:item>
<twig:Ui:tabs:item href="{{ path('dashboard_index', {filter: 'active'}) }}"
color="success"
current="{{ current_filter == 'active' }}">
Active
</twig:Ui:tabs:item>
<twig:Ui:tabs:item href="{{ path('dashboard_index', {filter: 'pending'}) }}"
color="warning"
current="{{ current_filter == 'pending' }}">
Pending
</twig:Ui:tabs:item>
<twig:Ui:tabs:item href="{{ path('dashboard_index', {filter: 'errors'}) }}"
color="error"
current="{{ current_filter == 'errors' }}">
Errors
</twig:Ui:tabs:item>
</twig:Ui:tabs>
{# Advanced features #}
<twig:Ui:tabs current="settings">
<twig:Ui:tabs:item href="/profile" current>
👤 Profile
</twig:Ui:tabs:item>
<twig:Ui:tabs:item href="/security" color="info">
🔒 Security
</twig:Ui:tabs:item>
<twig:Ui:tabs:item href="/billing" disabled>
💳 Billing (Coming Soon)
</twig:Ui:tabs:item>
</twig:Ui:tabs>
{# JavaScript/Stimulus Integration #}
<div data-controller="my-dashboard"
data-action="tabs:selected->my-dashboard#handleTabChange">
<twig:Ui:tabs current="{{ current_filter }}">
<twig:Ui:tabs:item href="{{ path('dashboard_index') }}"
data-dashboard-section="overview"
current="{{ current_filter == 'overview' }}">
📊 Overview
</twig:Ui:tabs:item>
<twig:Ui:tabs:item href="{{ path('dashboard_index', {section: 'analytics'}) }}"
data-dashboard-section="analytics"
color="info"
current="{{ current_filter == 'analytics' }}">
📈 Analytics
</twig:Ui:tabs:item>
</twig:Ui:tabs>
<div data-my-dashboard-target="content" class="mt-6">
<!-- Dynamic content loaded here -->
</div>
</div>
// assets/controllers/my_dashboard_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["content"]
connect() {
console.log("Dashboard controller connected")
}
// Handle tab selection events from the tabs component
handleTabChange(event) {
const { item, href, text } = event.detail
const section = item.dataset.dashboardSection
console.log(`Tab changed to: ${text} (${section})`)
// Update content based on selection
this.loadSection(section)
// Optional: Update URL without page reload
if (href && window.history) {
window.history.pushState({}, '', href)
}
}
loadSection(section) {
// Example: Load content via fetch
this.contentTarget.innerHTML = `<div class="animate-pulse">Loading ${section}...</div>`
fetch(`/api/dashboard/${section}`)
.then(response => response.text())
.then(html => {
this.contentTarget.innerHTML = html
})
.catch(() => {
this.contentTarget.innerHTML = `<div class="text-red-600">Error loading ${section}</div>`
})
}
// Public API: programmatically switch tabs
switchToTab(section) {
const tabsController = this.application.getControllerForElementAndIdentifier(
document.querySelector('[data-controller*="cisse--ui-bundle--tabs"]'),
'cisse--ui-bundle--tabs'
)
if (tabsController) {
const tabItem = document.querySelector(`[data-dashboard-section="${section}"]`)
if (tabItem) {
tabsController.setActiveTab(tabItem)
}
}
}
}
{# Real-time notifications with tab updates #}
<div data-controller="notifications"
data-action="tabs:selected->notifications#trackTabView">
<twig:Ui:tabs>
<twig:Ui:tabs:item href="/inbox" current>
📧 Inbox
<span data-notifications-target="inboxCount"
class="ml-2 bg-red-500 text-white rounded-full px-2 py-1 text-xs">
{{ unread_count }}
</span>
</twig:Ui:tabs:item>
<twig:Ui:tabs:item href="/sent" color="success">
📤 Sent
</twig:Ui:tabs:item>
</twig:Ui:tabs>
</div>
// assets/controllers/notifications_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["inboxCount"]
connect() {
// Setup real-time updates
this.setupWebSocket()
}
trackTabView(event) {
// Analytics tracking
const tabName = event.detail.text
gtag('event', 'tab_view', {
'tab_name': tabName,
'timestamp': Date.now()
})
}
setupWebSocket() {
// Example WebSocket for real-time count updates
this.ws = new WebSocket('/ws/notifications')
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'inbox_count') {
this.updateInboxCount(data.count)
}
}
}
updateInboxCount(count) {
if (this.hasInboxCountTarget) {
this.inboxCountTarget.textContent = count
this.inboxCountTarget.classList.toggle('hidden', count === 0)
}
}
}
<twig:Ui:table>
<twig:Ui:thead>
<twig:Ui:tr>
<twig:Ui:th>👤 User</twig:Ui:th>
<twig:Ui:th>📧 Email</twig:Ui:th>
<twig:Ui:th>📅 Joined</twig:Ui:th>
<twig:Ui:th>⚙️ Actions</twig:Ui:th>
</twig:Ui:tr>
</twig:Ui:thead>
<twig:Ui:tbody>
{% for user in users %}
<twig:Ui:tr>
<twig:Ui:td>
<div class="font-medium">{{ user.name }}</div>
</twig:Ui:td>
<twig:Ui:td>{{ user.email }}</twig:Ui:td>
<twig:Ui:td>{{ user.createdAt|date('M j, Y') }}</twig:Ui:td>
<twig:Ui:td>
<div class="flex gap-2">
<twig:Ui:button href="/users/{{ user.id }}" secondary size="sm">
View
</twig:Ui:button>
<twig:Ui:button href="/users/{{ user.id }}/edit" primary size="sm">
Edit
</twig:Ui:button>
</div>
</twig:Ui:td>
</twig:Ui:tr>
{% endfor %}
</twig:Ui:tbody>
</twig:Ui:table>
Create expandable table rows with the expandable prop on <twig:Ui:tr>. The expand button is automatically added as the first cell:
<twig:Ui:table>
<twig:Ui:thead>
<twig:Ui:th class="w-10"></twig:Ui:th> {# For the auto expand button #}
<twig:Ui:th>Name</twig:Ui:th>
<twig:Ui:th>Email</twig:Ui:th>
<twig:Ui:th>Status</twig:Ui:th>
</twig:Ui:thead>
{% for user in users %}
<twig:Ui:tr expandable :colspan="3">
{# No need to add expand button - it's automatic! #}
<twig:Ui:td>{{ user.name }}</twig:Ui:td>
<twig:Ui:td>{{ user.email }}</twig:Ui:td>
<twig:Ui:td>
<twig:Ui:badge color="success">Active</twig:Ui:badge>
</twig:Ui:td>
{# Expanded content block #}
<twig:block name="expanded">
<twig:Ui:code-block title="User Details">
{{ user|json_encode(constant('JSON_PRETTY_PRINT')) }}
</twig:Ui:code-block>
</twig:block>
</twig:Ui:tr>
{% endfor %}
</twig:Ui:table>
| Prop | Type | Default | Description |
|---|---|---|---|
expandable |
boolean | false |
Enable expandable row mode |
colspan |
number | 1 |
Number of data columns (expand button column is added automatically) |
defaultExpanded |
boolean | false |
Start in expanded state |
expandedClass |
string | '' |
Additional classes for expanded row |
Display code or JSON with optional title and copy button:
{# Basic JSON display #}
<twig:Ui:code-block title="Response Data">
{{ data|json_encode(constant('JSON_PRETTY_PRINT')) }}
</twig:Ui:code-block>
{# With copy button #}
<twig:Ui:code-block title="API Response" copyable>
{
"status": "success",
"message": "Data saved"
}
</twig:Ui:code-block>
{# Custom max height #}
<twig:Ui:code-block title="Large Data" maxHeight="20rem">
{{ largeData|json_encode(constant('JSON_PRETTY_PRINT')) }}
</twig:Ui:code-block>
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | null |
Optional header title |
language |
string | 'json' |
Code language (for syntax highlighting) |
maxHeight |
string | '12rem' |
Maximum height with scroll |
copyable |
boolean | false |
Show copy to clipboard button |
The bundle can be configured in config/packages/ux_components.yaml:
ux_components:
enabled: true # Default: true
CRITICAL: Your main CSS file must include these required elements:
@import "tailwindcss";
@source "../../vendor/cisse/ui-bundle"; /* ⚠️ MANDATORY - Bundle styles */
/* ⚠️ REQUIRED - Color variables for components to function */
@theme {
--color-primary: /* your primary color */;
--color-secondary: /* your secondary color */;
--color-primary-foreground: /* text color for primary backgrounds */;
--color-secondary-foreground: /* text color for secondary backgrounds */;
/* ... additional color variants */
}
All components include intelligent TailwindCSS class merging:
gehrisandro/tailwind-merge-php (if installed){# Example: Custom classes override component defaults #}
<twig:Ui:button class="bg-red-500" primary>
<!-- Results in proper primary button styling (not red) -->
Custom Button
</twig:Ui:button>
Built-in dark mode with CSS custom properties:
@import "tailwindcss";
@source "../../vendor/cisse/ui-bundle";
@custom-variant dark (&:is(.dark *));
@theme {
--color-primary: oklch(64.758% 0.19626 284.46);
--color-primary-50: oklch(100% 0 none);
--color-primary-100: oklch(100% 0 none);
--color-primary-200: oklch(96.104% 0.02008 292.15);
--color-primary-300: oklch(85.6% 0.07608 289.69);
--color-primary-400: oklch(74.93% 0.13633 287.4);
--color-primary-500: oklch(64.758% 0.19626 284.46);
--color-primary-600: oklch(52.771% 0.26674 276.96);
--color-primary-700: oklch(46.068% 0.30705 267.23);
--color-primary-800: oklch(38.845% 0.26206 266.82);
--color-primary-900: oklch(30.792% 0.20614 267.64);
--color-primary-950: oklch(26.578% 0.17671 268.39);
--color-secondary: oklch(29.515% 0.15616 273.84);
--color-secondary-50: oklch(57.49% 0.18681 281.61);
--color-secondary-100: oklch(53.218% 0.20688 279.56);
--color-secondary-200: oklch(45.476% 0.24238 273.8);
--color-secondary-300: oklch(40.165% 0.2236 272.43);
--color-secondary-400: oklch(34.814% 0.19049 273.08);
--color-secondary-500: oklch(29.515% 0.15616 273.84);
--color-secondary-600: oklch(21.742% 0.10552 275.99);
--color-secondary-700: oklch(13.321% 0.04444 281.55);
--color-secondary-800: oklch(0% 0 none);
--color-secondary-900: oklch(0% 0 none);
--color-secondary-950: oklch(0% 0 none);
--color-primary-foreground: oklch(0% 0 none);
--color-primary-foreground-50: oklch(47.478% 0 none);
--color-primary-foreground-100: oklch(43.86% 0 none);
--color-primary-foreground-200: oklch(36.002% 0 none);
--color-primary-foreground-300: oklch(28.094% 0 none);
--color-primary-foreground-400: oklch(19.125% 0 none);
--color-primary-foreground-500: oklch(0% 0 none);
--color-primary-foreground-600: oklch(0% 0 none);
--color-primary-foreground-700: oklch(0% 0 none);
--color-primary-foreground-800: oklch(0% 0 none);
--color-primary-foreground-900: oklch(0% 0 none);
--color-primary-foreground-950: oklch(0% 0 none);
--color-secondary-foreground: oklch(100% 0 none);
--color-secondary-foreground-50: oklch(100% 0 none);
--color-secondary-foreground-100: oklch(100% 0 none);
--color-secondary-foreground-200: oklch(100% 0 none);
--color-secondary-foreground-300: oklch(100% 0 none);
--color-secondary-foreground-400: oklch(100% 0 none);
--color-secondary-foreground-500: oklch(100% 0 none);
--color-secondary-foreground-600: oklch(91.583% 0 none);
--color-secondary-foreground-700: oklch(82.968% 0 none);
--color-secondary-foreground-800: oklch(74.123% 0 none);
--color-secondary-foreground-900: oklch(65.004% 0 none);
--color-secondary-foreground-950: oklch(60.325% 0 none);
--color-app: var(--color-secondary);
--color-app-50: var(--color-secondary-50);
--color-app-100: var(--color-secondary-100);
--color-app-200: var(--color-secondary-200);
--color-app-300: var(--color-secondary-300);
--color-app-400: var(--color-secondary-400);
--color-app-500: var(--color-secondary-500);
--color-app-600: var(--color-secondary-600);
--color-app-700: var(--color-secondary-700);
--color-app-800: var(--color-secondary-800);
--color-app-900: var(--color-secondary-900);
--color-app-950: var(--color-secondary-950);
--color-app-foreground: var(--color-secondary-foreground);
--color-app-foreground-50: var(--color-secondary-foreground-50);
--color-app-foreground-100: var(--color-secondary-foreground-100);
--color-app-foreground-200: var(--color-secondary-foreground-200);
--color-app-foreground-300: var(--color-secondary-foreground-300);
--color-app-foreground-400: var(--color-secondary-foreground-400);
--color-app-foreground-500: var(--color-secondary-foreground-500);
--color-app-foreground-600: var(--color-secondary-foreground-600);
--color-app-foreground-700: var(--color-secondary-foreground-700);
--color-app-foreground-800: var(--color-secondary-foreground-800);
--color-app-foreground-900: var(--color-secondary-foreground-900);
--color-app-foreground-950: var(--color-secondary-foreground-950);
}
@layer base {
.dark {
--color-app: var(--color-primary);
--color-app-50: var(--color-primary-50);
--color-app-100: var(--color-primary-100);
--color-app-200: var(--color-primary-200);
--color-app-300: var(--color-primary-300);
--color-app-400: var(--color-primary-400);
--color-app-500: var(--color-primary-500);
--color-app-600: var(--color-primary-600);
--color-app-700: var(--color-primary-700);
--color-app-800: var(--color-primary-800);
--color-app-900: var(--color-primary-900);
--color-app-950: var(--color-primary-950);
--color-app-foreground: var(--color-primary-foreground);
--color-app-foreground-50: var(--color-primary-foreground-50);
--color-app-foreground-100: var(--color-primary-foreground-100);
--color-app-foreground-200: var(--color-primary-foreground-200);
--color-app-foreground-300: var(--color-primary-foreground-300);
--color-app-foreground-400: var(--color-primary-foreground-400);
--color-app-foreground-500: var(--color-primary-foreground-500);
--color-app-foreground-600: var(--color-primary-foreground-600);
--color-app-foreground-700: var(--color-primary-foreground-700);
--color-app-foreground-800: var(--color-primary-foreground-800);
--color-app-foreground-900: var(--color-primary-foreground-900);
--color-app-foreground-950: var(--color-primary-foreground-950);
}
}
@layer utilities {
/* For Remove Date Icon */
input[type="date"]::-webkit-inner-spin-button,
input[type="time"]::-webkit-inner-spin-button,
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
display: none;
-webkit-appearance: none;
}
}
⚠️ Critical: The
@source "../../vendor/cisse/ui-bundle";directive is mandatory and must be included in your CSS file for the components to work properly.
Example:
{# Classes are automatically merged, with later classes taking precedence #}
<twig:Ui:button class="bg-red-500" primary>
{# Results in proper primary styling, not red #}
</twig:Ui:button>
All components accept standard HTML attributes plus component-specific props:
<twig:Ui:button
type="submit"
primary
size="lg"
class="mt-4 shadow-lg"
data-turbo="false"
id="submit-btn"
disabled="{{ not form.valid }}">
🚀 Submit Form
</twig:Ui:button>
Override any component by creating templates in your application:
templates/
└── components/
└── ux/
├── button.html.twig # Custom button styling
├── card.html.twig # Custom card layout
└── input/
└── text.html.twig # Custom text input
Customize the entire design system by modifying CSS variables:
@theme {
/* Brand colors */
--color-primary: oklch(/* your brand color */);
--color-secondary: oklch(/* your accent color */);
/* Component-specific overrides */
--ui-button-radius: 12px;
--ui-input-border: 2px solid theme(colors.gray.300);
}
We welcome contributions! To get started:
Clone the repository
git clone https://github.com/cisse/ui-bundle.git
cd ui-bundle
Install dependencies
composer install
npm install # For TailwindCSS compilation
Run tests
composer test
php bin/phpunit
Code standards
composer cs-fix # Fix coding standards
composer analyze # Run static analysis
src/
├── Components/ # Twig component classes
├── Resources/
│ ├── views/ # Component templates
│ └── assets/ # CSS and JS assets
└── UiBundleBundle.php # Bundle configuration
This bundle is released under the MIT License. See the LICENSE file for details.
Made with ❤️ for the Symfony community
⭐ Star this repo if you find it useful!
How can I help you explore Laravel packages today?