A type-safe PHP library for standards-compliant HTML tag rendering
Build and render block, inline, input, and void elements with immutable fluent APIs.
composer require ui-awesome/html-core:^0.8Renders begin/end tags and full elements using standards-compliant tag enums.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Html;
use UIAwesome\Html\Interop\{Block, Inline, Voids};
echo Html::begin(Block::DIV, ['class' => 'container']);
// <div class="container">
echo Html::inline(Inline::SPAN, 'Hello');
// <span>Hello</span>
echo Html::end(Block::DIV);
// </div><?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Html;
use UIAwesome\Html\Interop\Block;
$content = '<span>Test Content</span>';
echo Html::element(Block::DIV, $content, ['class' => 'test-class']);
// <div class="test-class">
// <span>Test Content</span>
// </div>
echo Html::element(Block::DIV, $content, ['class' => 'test-class'], true);
// <div class="test-class">
// <span>Test Content</span>
// </div>Void tags render without closing tags. Complex attributes (like class arrays and data arrays) are rendered via the
installed ui-awesome/html-helper dependency.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Html;
use UIAwesome\Html\Interop\Voids;
echo Html::void(
Voids::IMG,
[
'class' => ['void'],
'data' => ['role' => 'presentation'],
],
);
// <img class="void" data-role="presentation">Input elements render the input tag with optional prefix and suffix segments through the same template primitives used by inline elements.
<?php
declare(strict_types=1);
namespace App;
use BackedEnum;
use UIAwesome\Html\Core\Element\BaseInput;
use UIAwesome\Html\Interop\{Inline, Voids};
final class SearchInput extends BaseInput
{
protected function getTag(): BackedEnum
{
return Voids::INPUT;
}
protected function run(): string
{
return $this->buildElement();
}
}
echo SearchInput::tag()
->type('search')
->name('q')
->prefix('Search')
->prefixTag(Inline::LABEL)
->render();
// <label>Search</label>
// <input name="q" type="search">Create your own element classes by extending the provided base elements.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Element\BaseBlock;
use UIAwesome\Html\Interop\Block;
use BackedEnum;
final class Div extends BaseBlock
{
protected function getTag(): BackedEnum
{
return Block::DIV;
}
}
echo Div::tag()
->class('card')
->content('Content')
->render();
// <div class="card">
// Content
// </div>BaseBlock implements ClearableContentInterface. Use $element->clearContent() to obtain an empty-content clone
without changing attributes or the original instance. Chain ->content($value) to append encoded replacement text
or ->html($trustedHtml) to append trusted raw HTML. Calling content('') alone does not clear existing content.
BaseBlock supports stack-based begin/end rendering, with protection against mismatched tags.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Element\BaseBlock;
use UIAwesome\Html\Interop\Block;
use BackedEnum;
final class Div extends BaseBlock
{
protected function getTag(): BackedEnum
{
return Block::DIV;
}
}
echo Div::tag()->begin();
echo 'Nested Content';
echo Div::end();
// <div>
// Nested Content
// </div>Inline elements can render prefix and suffix segments, optionally wrapped in their own tags.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Element\BaseInline;
use UIAwesome\Html\Interop\Inline;
use BackedEnum;
final class Span extends BaseInline
{
protected function getTag(): BackedEnum
{
return Inline::SPAN;
}
protected function run(): string
{
return $this->buildElement($this->getContent());
}
}
echo Span::tag()
->content('Content')
->prefix('Prefix')
->prefixTag(Inline::STRONG)
->suffix('Suffix')
->suffixTag(Inline::EM)
->render();
// <strong>Prefix</strong>
// <span>Content</span>
// <em>Suffix</em>Use an immutable Config instance to apply a design-system recipe without global mutable state. Multiple configs can be
used in the same process, and fluent calls made after config() are local overrides.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Config\{Call, ComponentContext, Config, Cookbook, Recipe};
use UIAwesome\Html\Core\Element\BaseInline;
use UIAwesome\Html\Core\Theme\ThemeInterface;
use UIAwesome\Html\Interop\Inline;
use BackedEnum;
final class Span extends BaseInline
{
protected function getTag(): BackedEnum
{
return Inline::SPAN;
}
protected function run(): string
{
return $this->buildElement($this->getContent());
}
}
final readonly class FlowbiteTheme implements ThemeInterface
{
public function getName(): string
{
return 'flowbite';
}
public function getRecipes(ComponentContext $context): iterable
{
if ($context->component === 'badge') {
yield new Recipe(
'flowbite.badge',
new Cookbook(new Call('class', 'rounded text-sm')),
);
}
}
}
$config = new Config(new FlowbiteTheme());
echo Span::tag()
->config($config, new ComponentContext('badge'))
->id('badge-1')
->content('New')
->render();
// <span class="rounded text-sm" id="badge-1">New</span>Theming is dependency injection, not global state. Build one Config in the application container from the theme
and the applier, inject it where components are created, and apply it per component with a ComponentContext. The
library holds no static or global theme registry, so parallel tests, multiple tenants, and several design systems can
coexist in the same process.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Config\{ComponentContext, Config, ConfigApplier};
// Container definition, built once per application.
$container->set(
Config::class,
static fn(): Config => new Config(new FlowbiteTheme(), new ConfigApplier()),
);
// Consumer, receiving the config by injection.
final readonly class Badge
{
public function __construct(private Config $config) {}
public function render(string $label): string
{
return Span::tag()
->config($this->config, new ComponentContext('badge'))
->content($label)
->render();
}
}Fluent calls made after config() are local overrides, and each Config instance applies only its own recipes.
The component identifier, the variant, size, and scheme qualifiers, the states, and hasState() also accept a
string-backed enum. The context stores the backing value, so themes keep comparing plain strings. An int-backed enum
throws InvalidArgumentException.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Config\ComponentContext;
enum BadgeName: string
{
case BADGE = 'badge';
case PILL = 'pill';
}
$context = new ComponentContext(BadgeName::BADGE, variant: BadgeName::PILL, states: ['active']);
$context->component; // 'badge'
$context->variant; // 'pill'
$context->hasState('active'); // trueFor a simpler approach without a theme, override loadDefault() in your tag class. These defaults are applied
automatically when tag() is called.
<?php
declare(strict_types=1);
namespace App;
use UIAwesome\Html\Core\Element\BaseBlock;
use UIAwesome\Html\Interop\Block;
use BackedEnum;
final class Container extends BaseBlock
{
protected function getTag(): BackedEnum
{
return Block::DIV;
}
protected function loadDefault(): array
{
return [
'class' => 'container',
];
}
}
echo Container::tag()->render();
// <div class="container">
// </div>
echo Container::tag(['class' => 'container-fluid'])->render();
// <div class="container container-fluid">
// </div>Each key in loadDefault() and in the arrays passed to tag() must name a public instance method or property of the
tag, and a method must return the tag, as fluent setters do. A numeric key, such as a value listed without its method
name, an unknown key, such as a misspelled method name, or a method returning another value, such as render, throws
ConfigException.
Configuration priority (from weakest to strongest):
- Class defaults from
loadDefault() - Defaults passed to
tag() - Application-scoped recipes applied by
config() - Fluent local overrides called after
config()
This library is agnostic and designed to be extended. You can define your own tag collections (for example, for SVG, MathML, or Web Components) with custom string-backed enums.
Html::element()handles generic open/content/close rendering.Html::inline()handles inline rendering.Html::void()handles void rendering.
You can create a custom enum for your specific domain and use it with html-core.
enum SvgTag: string
{
case SVG = 'svg';
case G = 'g';
// ... add other SVG block tags as needed
}
// now you can use it with the Html renderer or your custom classes
echo Html::element(SvgTag::G, '...');
// <g>...</g>For detailed configuration options and advanced usage.