Getting started: PHP
The php/ package (Composer, PHP 8.1+) renders static components on the server: the same markup as React, plus valyro-ui/styles.css and the theme attributes — no Node or React at runtime
1. Add the package
Tested on PHP 8.5.9 with Composer 2 (the code targets PHP 8.1+). The ui-kit/ui-kit package isn't on Packagist yet: add it from the repository as a path repository. Locales other than en-US need the intl extension (without it, numbers are formatted as in en-US).
{
"repositories": [{ "type": "path", "url": "../valyro-ui/php" }],
"require": { "ui-kit/ui-kit": "@dev" }
}
2. Styles and theme
Take valyro-ui/styles.css from the package (npm run build:core → dist/core/styles.css) and put it with your static files. Theme::htmlAttributes() sets data-theme/data-mode on <html>; the system mode outputs light plus a small script in <head> (Theme::head()) that switches to dark before the first paint if the OS is set to dark. For CSP, pass a nonce as the third argument. The stylesheet has no @font-face: for the vercel theme also link fonts/geist.css and fonts/geist-mono.css, copied with their font folders as in the HTML + CSS guide.
<?php
require __DIR__ . '/../vendor/autoload.php';
use UiKit\Html;
use UiKit\Theme;
use UiKit\Ui;
?>
<!doctype html>
<html lang="en" <?= Theme::htmlAttributes('glass', 'system') ?>>
<head>
<meta charset="utf-8">
<?= Theme::head('/valyro-ui.css', 'system') ?>
</head>
<body>
<?= Ui::HeroSection([
'title' => 'Hello from PHP',
'subtitle' => $_GET['name'] ?? 'Same markup as the React components.',
'primaryAction' => ['label' => 'Get started', 'href' => '/start', 'icon' => Ui::icon('Zap')],
]) ?>
<?= Ui::Card([],
Ui::CardHeader([], Ui::CardTitle([], 'Project'), Ui::CardDescription([], 'Production')),
Ui::CardContent([], Ui::Badge(['color' => 'green', 'dot' => true], 'Ready')),
Ui::CardFooter([], Ui::Button(['asChild' => true, 'variant' => 'primary'], Html::el('a', ['href' => '/open'], 'Open'))),
) ?>
</body>
</html>
3. Component API
Props match React: the same names and defaults (className or class; style as an array). Content: strings (escaped), numbers, arrays, other components, Html::el(), and Html::raw() for trusted markup only. Callbacks (formatValue, ratingLabel, ellipsisLabel) are callables. Icons: Ui::icon('ArrowRight', ['size' => 14]). Any component by name: Ui::create('Badge', [...]). All strings are escaped (& < > " '), on* handlers and unsafe attribute names are dropped, and URLs with schemes like javascript:/data: are replaced with #.
4. What works without JavaScript
Static components: Button, IconButton, ButtonGroup, Badge, Avatar and AvatarGroup, Separator, Skeleton and SkeletonText, Spinner, ProgressBar, Breadcrumbs, TextInput, Textarea, Card and its parts, StatsCard (with a sparkline), Alert, EmptyState, Timeline, HeroSection, FeatureGrid, Testimonials, LogoCloud, CTASection, Footer. PHP renders Accordion, Tabs, FAQSection and PricingTable in their initial state (what React renders on the server); switching is up to React: mount the component over the markup, as the WordPress plugin does. MarkdownPreview, TokenUsage and UsageQuota aren't ported to PHP.
5. Tests
npm run php:fixtures renders a set of props with renderToStaticMarkup into php/tests/fixtures/components.json, and PHPUnit compares the PHP output byte for byte. Two intentional differences are normalized: ids from useId (React derives them from the position in the tree, PHP from the order on the page) and javascript: links (React substitutes code that throws, PHP uses #). The XSS tests live there too.
cd php
composer install
vendor/bin/phpunit