Skip to content

Core Concepts

Berry is mostly about composing correct HTML strings with small element builders.

Instead of writing markup in a separate template language, you build elements with typed PHP objects. That gives you normal PHP tooling: autocomplete, refactoring, type checks, and static analysis.

The rendered result is still just HTML, but the path to that string is more structured than manually appending fragments together.

Berry tries to make a few things correct by default:

  • text goes through escaping when you use text()
  • attributes are escaped by default
  • common attributes have named helpers
  • components have normal PHP signatures
  • invalid method calls can be caught by your editor or PHPStan

The base interface is Berry\Element.

An element has a toString() method and can be cast to a string.

interface Element extends Stringable
{
public function toString(): string;
}

Tags, components, fragments, text nodes, SVG elements, and XML elements are all elements.

Most HTML elements are represented by tags.

use function Berry\Html\button;
$button = button()
->type('submit')
->class('btn')
->text('Save');

This renders:

<button type="submit" class="btn">Save</button>

There are two important tag types internally:

  • Tag renders an opening tag, body, and closing tag
  • VoidTag renders a tag without children, like input() or img()

You normally do not need to create these classes yourself. Use the helper functions instead.

Berry ships helper functions for most HTML elements.

use function Berry\Html\{article, h2, p};
$post = article()
->child(h2()->text('Hello'))
->child(p()->text('This is a post.'));

These functions return typed element objects, so elements can expose useful methods like href(), src(), type(), disabled(), and so on.

You are not just moving strings around, you are calling methods on concrete element builders.

Elements can have children.

use function Berry\Html\{li, ul};
$list = ul()
->child(li()->text('First'))
->child(li()->text('Second'));

Children are rendered into the parent element.

If you already have a list of elements, use children().

$items = ['First', 'Second', 'Third'];
$list = ul()->children(array_map(
fn (string $item) => li()->text($item),
$items,
));

For multiple elements without a wrapper, use a fragment.

use function Berry\fragment;
use function Berry\Html\{h1, p};
$content = fragment(
h1()->text('Title'),
p()->text('Body'),
);

Components are classes for rendering more complex UI elements.

They let you hide a whole tree of elements behind a small PHP API. This is useful when a piece of UI has enough structure, state, or behaviour that a plain function starts to feel cramped.

use Berry\Component;
use Berry\Element;
use function Berry\Html\button;
final class CounterButton extends Component
{
public function __construct(
private int $value,
) {
}
protected function renderComponent(): Element
{
return button()
->class('btn')
->text('+' . $this->value);
}
}

Because components are just PHP classes, they can also expose their own methods and act like small builders themselves.

use Berry\Component;
use Berry\Element;
use function Berry\Html\p;
final class Greeter extends Component
{
private ?string $name = null;
public function __construct(
private string $greeting = "Hello",
) {
}
public function greet(string $name): self
{
$this->name = $name;
return $this;
}
protected function renderComponent(): Element
{
return p()->text($this->greeting . ' ' . $this->name);
}
}
echo new Greeter(greeting: 'Hallo')
->greet(name: 'Peter')
->toString();

This is not something every component needs, but it can be a nice way to express more domain-specific UI builders without dropping back to string concatenation.

Berry renders HTML strings.

When you call methods like text(), child(), or attr(), the object stores enough information to render the final HTML. When you call toString(), the element returns the HTML string.

The difference from hand-written strings is that most mistakes become normal PHP mistakes. Misspelled methods, wrong argument types, missing classes, and invalid component constructor calls can be found by your editor or PHPStan.

This is the main trade: you still get plain HTML at the end, but the code that creates it is easier to check for correctness.

There is no client runtime. If you need interactivity, pair Berry with normal browser APIs, Alpine, HTMX, or whatever you already use.

Berry is intentionally boring here. It works well with hypermedia tools like HTMX and Datastar because those tools want server-rendered HTML anyway.

This also helps with Locality of Behaviour. A component can keep its structure, styling hooks, and hypermedia attributes in one place, instead of scattering the behaviour across several files.