stevebauman/hypertext
Hypertext is a Laravel package for building HTML with PHP objects. Define reusable components as classes, compose them with slots and attributes, and render clean markup without template files—an object-oriented alternative to Blade for dynamic UI.
Install the package via Composer:
composer require stevebauman/hypertext
Laravel's package discovery registers the service provider automatically. If your application does not use discovery, register the provider manually in config/app.php.
Look first at:
src/Hypertext.php for the entry point.src/Element.php (or equivalent) for attribute and child behavior.The API is deliberately small: create an element, add attributes, add children or text, then render it. The snippets below use the package's fluent shape (make(), element methods, attribute(), text(), children(), render()); confirm exact method names against the README for your pinned version.
First use case: replace a messy Blade partial with a reusable PHP component. For example, a button:
use Stevebauman\Hypertext\Hypertext;
$button = Hypertext::make()
->a()
->attribute('href', route('posts.show', $post))
->attribute('class', 'btn btn-primary')
->text($post->title)
->render();
Then in Blade:
{!! $button !!}
Once the basic element works, move the construction into a dedicated class or Blade component so the markup is defined once and reused everywhere.
Treat Hypertext components the same way you treat Blade components: one class per UI concept.
final class Alert
{
public function __construct(
private string $message,
private string $type = 'info',
) {}
public function render(): string
{
return Hypertext::make()
->div()
->attribute('class', "alert alert-{$this->type}")
->attribute('role', 'alert')
->text($this->message)
->render();
}
}
Usage:
{!! (new Alert($message, 'danger'))->render() !!}
This keeps conditionals, attributes, and escaping rules in one testable place instead of scattering them across Blade templates.
Nesting is where the object API becomes clearer than string concatenation.
$card = Hypertext::make()
->div()
->attribute('class', 'card')
->children([
Hypertext::make()
->div()
->attribute('class', 'card-body')
->children([
Hypertext::make()->h5()->attribute('class', 'card-title')->text($title),
Hypertext::make()->p()->attribute('class', 'card-text')->text($body),
]),
]);
For collections, map your models into child elements and pass the resulting array to children():
$items = $posts->map(
fn ($post) => Hypertext::make()
->li()
->attribute('class', 'list-group-item')
->text($post->title)
)->all();
$list = Hypertext::make()
->ul()
->attribute('class', 'list-group')
->children($items);
Use null or false to omit optional attributes, and true for boolean attributes, depending on the package's rendering rules.
$input = Hypertext::make()
->input()
->attribute('type', 'text')
->attribute('name', 'email')
->attribute('value', old('email'))
->attribute('required', $required ? true : null)
->attribute('disabled', $disabled ? true : null);
For more complex conditions, use a when()-style helper if available, or build the attribute array before constructing the element:
$attributes = [
'class' => 'form-control',
'id' => 'email',
];
if ($hasError) {
$attributes['class'] .= ' is-invalid';
$attributes['aria-describedby'] = 'email-error';
}
Then pass them through the package's bulk attribute method if one exists, or loop over them:
$element = Hypertext::make()->input();
foreach ($attributes as $name => $value) {
$element->attribute($name, $value);
}
If your Hypertext element implements Laravel's Htmlable, return it directly from a Blade component's render() method and use normal Blade echoing:
public function render(): Htmlable
{
return Hypertext::make()
->button()
->attribute('type', 'submit')
->attribute('class', 'btn btn-success')
->text($this->label);
}
If your component returns a plain string, Blade's {{ }} will escape the markup. In that case either wrap the result in Illuminate\Support\HtmlString or intentionally use {!! !!} for trusted, self-generated markup.
Hypertext makes markup assertions straightforward because you can render a component in isolation.
public function test_button_renders_expected_markup(): void
{
$button = new Button('Save', '/save', 'primary');
$html = $button->render();
$this->assertStringContainsString('href="/save"', $html);
$this->assertStringContainsString('class="btn btn-primary"', $html);
$this->assertStringContainsString('Save', $html);
}
For Laravel response tests, assertSee() escapes by default. If you are asserting raw generated markup, pass false:
$response->assertSee('<a href="/save"', false);
e() or htmlspecialchars() before passing it in will double-escape output. Pass raw model values and let the package handle escaping.Htmlable, {{ $element }} is safe and correct. If it returns a plain string, {{ $element }} will display escaped tags. Standardize on one convention in your codebase.false, null, true, and empty strings render for boolean attributes. Behavior can differ between disabled, checked, selected, and normal string attributes.composer.json and check the README for that tag rather than assuming the latest docs match.->render() or dd($element) at each step while composing. Inspect the final string for missing attributes, unexpected escaping, and unclosed children.How can I help you explore Laravel packages today?