Install xph
Create a project and install xph from npm as a development dependency.
mkdir my-xph-app
cd my-xph-app
npm init -y
npm install -D xph
mkdir src
Your next component system
Type-safe components with scoped styles, compiled to native PHP. Auto-escaped by default.
Deploy anywhere PHP runs.
---
export interface Props {
name: string;
}
export interface Server {
greeting: string;
}
---
<script is:server>
return [
'greeting' => 'Hello, ' . $props['name'],
];
</script>
<h1>{server.greeting}</h1>
<style>
h1 {
color: olive;
}
</style>
01 / Getting started
Use Node.js for development and PHP to check and render your components.
Create a project and install xph from npm as a development dependency.
mkdir my-xph-app
cd my-xph-app
npm init -y
npm install -D xph
mkdir src
Create src/Welcome.xph in your new project.
Declare the props your component accepts and the data PHP returns. Use that data in your component; xph escapes dynamic text and scopes your styles.
---
export interface Props {
name: string;
}
export interface Server {
greeting: string;
}
---
<script is:server>
return [
'greeting' => 'Hello, ' . $props['name'],
];
</script>
<h1>{server.greeting}</h1>
<style>
h1 {
color: olive;
}
</style>
Create xph.config.ts in your project root to set your source and
output directories.
From your project root, run
npx xph check to check contracts and PHP syntax, then
npx xph build to generate PHP and assets. Use
npx xph dev
to rebuild as you edit.
export default {
srcDir: 'src',
outDir: 'dist',
assetBase: '/assets/',
compressHTML: true,
scopedStyleStrategy: 'attribute',
php: 'php',
debug: false,
};
Create dist/index.php to render your component.
Run php -S localhost:8080 -t dist from your project root and open
http://localhost:8080 to see “Hello, world”.
Copy dist/ to your PHP server to deploy.
<?php
require __DIR__ . '/xph.php';
// Prepare all page components before emitting styles.
$renderer = \Xph\renderer(['Welcome']);
// Emit CSS once; place in your document head.
echo $renderer->styles();
// src/Welcome.xph is registered as Welcome.
echo $renderer->render('Welcome', ['name' => 'world']);
// Emit bundled modules once, after the content.
echo $renderer->scripts();
02 / Syntax
The supported component syntax, from typed inputs to browser scripts. Snippets use
your declared props; imports and declarations are shown where needed.
Put imports, constants, and helper functions between --- fences.
They run during compilation. Helper calls in templates also run at build time
when all arguments are build-time values; their results must be serializable.
Build-time code also supports expression statements, if,
for, for…of, and while.
---
const name = 'xph';
function upper(value: string) {
return value.toUpperCase();
}
---
<h1>{upper(name)}</h1>
Declare inputs with Props and read them through props.
Contracts support primitives, arrays, records, aliases, and unions, including
imported and generic types that resolve to supported shapes. Types are checked
at build time; PHP values are not validated at runtime.
---
export interface Props {
title: string;
size?: 'small' | 'large';
items: { name: string; price: number }[];
}
---
<h1>{props.title}</h1>
One server script runs before each render. Read $props and return
an array matching Server. Use PHP for request-time work; omit
<?php tags.
Set debug: true to preserve original exceptions and include source
diagnostics. It defaults to false.
---
export interface Server {
year: string;
}
---
<script is:server>
return [
'year' => date('Y'),
];
</script>
<p>{server.year}</p>
Declare top-level methods or function properties on Server, then
return PHP closures. Parameters and return values need explicit types;
optional/rest parameters and nested methods are unsupported. Calls run when the
expression renders. PHP numeric parameters should accept float or
int|float.
---
export interface Server {
upper(value: string): string;
}
---
<script is:server>
return [
'upper' => static fn(string $value): string => strtoupper($value),
];
</script>
<h1>{server.upper('hello')}</h1>
Put values or expressions in braces to insert text, join strings, or calculate
numbers with + - * / **. Dynamic text is HTML-escaped. Strings and
numbers render as text; booleans, null, and
undefined render nothing. Arrays and objects must be used in a
list, spread, or other supported expression.
<h1>{props.title}</h1>
<p>{'Hello, ' + props.name}</p>
<p>Total: {props.price * props.quantity}</p>
Use dot or bracket access, including array indexes. Optional access
?. handles missing values; ?? supplies a fallback for
null or undefined. Array length is supported; string indexing and
string length are not.
<p>{props.user?.name ?? 'Guest'}</p>
<p>{props.items[0] ?? 'Empty'}</p>
<p>{props.items.length}</p>
Use + - * / ** and < <= > >= with numbers;
+ also joins two strings. Use === !== for scalar
equality, ! && || for booleans, and unary
+ - for numbers. Parentheses group expressions.
<p>{(props.price * props.quantity) + 5}</p>
<p>{props.status === 'ready' ? 'Ready' : 'Waiting'}</p>
<button disabled={!props.ready || props.busy}>Save</button>
Use && to show content, || to show a fallback,
or a ternary to choose a branch. Conditions must be booleans; compare counts
explicitly.
{props.visible && <p>Hello!</p>}
{props.visible || <p>Nothing to show.</p>}
{props.signedIn ? <a href="/account">Account</a> : <p>Sign in</p>}
Use .map() with an expression-bodied arrow callback. Its optional
second parameter is the zero-based index. Prepare filtered or sorted
request-time lists in PHP.
<ul>
{props.items.map((item, index) => <li>{index + 1}. {item}</li>)}
</ul>
{props.items.length === 0 && <p>No items yet.</p>}
Write ordinary HTML, SVG, custom elements, and HTML comments. Comments remain in
the output. URL attributes are checked for unsafe schemes; invalid attributes,
event handlers, and srcdoc are rejected.
<!-- This comment appears in the rendered HTML. -->
<svg viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
</svg>
<my-element data-label="Hello"></my-element>
Static whitespace is compressed by default; dynamic values and
whitespace-sensitive content such as <pre> are preserved. Set
compressHTML: false to keep template spacing as written.
<!-- Expressions preserve the supplied whitespace. -->
<pre>{'First line\n Indented line'}</pre>
Use native names such as class and for. Boolean HTML
attributes use presence; ordinary false/nullish values omit the attribute. ARIA
and data booleans become "true" or "false". Empty
strings and zero are preserved.
style accepts a CSS string or an object.
<label for="name">Name</label>
<input id="name" disabled={props.disabled} />
<p aria-hidden={props.disabled}>Status</p>
<div
style={{
backgroundColor: 'navy',
'font-size': '14px',
'--accent': 'gold',
opacity: 0.8,
}}
/>
Spread an object onto HTML or a component. Later values override earlier ones. HTML attribute values must be scalar; component props can contain arrays and records matching their contracts.
<button {...props.attrs} id="save">Save</button>
<Card {...{ title: props.title }} />
Use an uppercase binding or a property containing an HTML tag string. Dynamic tags support attributes, content directives, and scoped styles. They select HTML elements, not components; executable or document-changing tags such as script, style, and iframe are restricted.
---
export interface Props { tag: 'section' | 'article' }
const Tag = 'section';
---
<Tag>Fixed tag</Tag>
<props.tag>Tag from props</props.tag>
Import a .xph component with a default binding and pass its
declared props. Imports must stay inside your project; circular component
imports are rejected.
---
import Card from './Card.xph';
---
<Card title="Hello" />
Use <xph.self> to render the current component without an
import. Props are checked against its Props interface. Add a
condition that stops recursion.
---
export interface Props { depth: number }
---
<p>{props.depth}</p>
{props.depth > 0 && <xph.self depth={props.depth - 1} />}
In the child component, use <slot> for content passed between
its opening and closing tags. Optional fallback content appears when the slot is
not supplied.
<!-- Card.xph -->
<article>
<slot>Nothing here yet.</slot>
</article>
<!-- Parent (imports Card) -->
<Card>
<p>Inside the card</p>
</Card>
Match a child’s literal slot name with
<slot name="…">. Assigned elements must be direct children of
an imported component. Multiple children with the same name render in order.
<!-- Card.xph -->
<slot name="header">Default heading</slot>
<!-- Parent (imports Card) -->
<Card>
<h2 slot="header">News</h2>
</Card>
xph.slots.render(name, args?) renders a slot to an HTML string at
PHP request time. Missing slots return an empty string. Interpolation escapes
the result; use set:html to insert its markup. Each call renders
the slot again.
<div set:html={xph.slots.render('default')} />
<header set:html={xph.slots.render('header')} />
Pass positional arguments as an array to xph.slots.render(). The
parent supplies an expression-bodied callback with explicitly typed parameters.
For a named callback slot, wrap it in <Fragment slot="…">.
Ordinary slot content ignores arguments; callback argument types are not checked
across components.
<!-- List.xph (items: string[] in Props) -->
{props.items.map(item => (
<p set:html={xph.slots.render('default', [item])} />
))}
<!-- Parent (imports List) -->
<List items={['Hello', 'World']}>
{(item: string) => <strong>{item}</strong>}
</List>
Group elements without adding an HTML wrapper using <> or
<Fragment>. Built-in Fragment accepts only an
optional slot attribute, useful for grouping named content.
<>
<h2>Title</h2>
<p>Details</p>
</>
<Card>
<Fragment slot="header">
<h2>News</h2>
<p>Today’s updates</p>
</Fragment>
</Card>
class:list combines strings, numbers, nested arrays, and truthy
object keys with an ordinary class. False, nullish, and zero
entries are skipped. On components it passes the resulting string as a
class prop.
<div class="panel" class:list={[
'rounded', ['bordered'], { active: props.active },
]} />
set:text inserts escaped text; set:html inserts
trusted or sanitized HTML without escaping. Use one directive on an HTML element
without children. They do not work on components, slots, Fragment, scripts, or
styles.
<h2 set:text={props.title} />
<div set:html={props.html} />
<style> scopes selectors and keyframe names to the component.
is:global disables scoping. CSS is bundled into external assets;
use PHP’s \Xph\assets() to include them, as shown in Getting
Started.
<style>
h1 { color: olive; }
</style>
<style is:global>
body { margin: 0; }
</style>
Set scopedStyleStrategy to 'attribute' (default),
'class', or 'where'. Use 'where' to scope
styles without adding selector specificity, making them easier to override.
// xph.config.ts
export default {
srcDir: 'src',
scopedStyleStrategy: 'where',
};
Plain scripts run in the browser and support TypeScript and imports. Use event
listeners instead of HTML event-handler attributes. Scripts are bundled into
external assets; include their URLs through \Xph\assets().
<button id="save">Save</button>
<script>
document.querySelector('#save')?.addEventListener('click', () => {
console.log('Saved');
});
</script>
Add is:inline to render a script or style in place. Content and
static attributes stay as written; imports and URLs are left unchanged. Inline
assets skip bundling, type checking, CSS scoping, and deduplication, and are
excluded from \Xph\assets(). Use plain JavaScript; dynamic
attributes are unsupported.
<script is:inline>
console.log('Hello');
</script>
<script is:inline src="/browser.js" defer></script>
<style is:inline>
body { margin: 0; }
</style>
Browser scripts cannot access props, server, or
frontmatter bindings. Pass scalar data through HTML data attributes and read it
from the DOM. Runtime interpolation inside scripts and styles is unsupported.
<p id="greeting" data-name={props.name}></p>
<script>
const el = document.querySelector<HTMLElement>('#greeting');
if (el) el.textContent = 'Hello, ' + el.dataset.name;
</script>
03 / In your workflow
Use xph components in your PHP application, WordPress theme, or Laravel project, with editor support for your workflow.
Import .xph components into .astro pages with the
astro-wordpress
adapter. Build PHP pages with component styles and scripts included automatically
and deduplicated per page. Use static Astro output and serve the build with PHP.
Render components from Blade with @xph. Middleware adds dependency
CSS and JavaScript automatically, deduplicated per request.
Syntax highlighting, diagnostics, completion, hover, and navigation for typed props and HTML attributes. Add an optional PHP language server for IntelliSense, code actions, and formatting inside server blocks.
Set up VS Code ↗A plugin for Neovim 0.11+ with .xph detection, highlighting, and language server setup. The shared language server includes contract and HTML attribute completion, hover, diagnostics, and component navigation.
Set up Neovim ↗