Skip to content

Columns in Kirbytext

KirbyTags are really great for use cases like adding images or links, but they come to a limit if you want to nest KirbyTags or want to add more than just a single line of text.

One such use case is adding columns inside a textarea field. In this recipe, we show you how to solve such uses with KirbyTag Hooks. When we are ready, you can add columns like this:


Left column


Right column


We use a starting and an ending tag, and columns are separated by a ++++ separator. The number of columns is not limited. Just add more ++++ separators for more columns.


KirbyText has a feature called pre and post filters. Pre filters are applied to text before KirbyTags and Markdown is parsed. Post filters are applied afterwards.

The filter plugin

KirbyTag Hooks are callbacks, which can be added to the kirbytags:before or kirbytags:after keys. Those callbacks can receive multiple arguments, here we only need the first parameter $text, which contains the raw text, which can be modified and must be returned.

Our final result looks like this:

Kirby::plugin('kirby/columns', [
  'hooks' => [
    'kirbytags:before' => function ($text) {

      $text = preg_replace_callback('!\(columns(…|\.{3})\)(.*?)\((…|\.{3})columns\)!is', function($matches) use($text) {

        $columns        = preg_split('!(\n|\r\n)\+{4}\s+(\n|\r\n)!', $matches[2]);
        $html           = [];
        $classItem      = option('kirby.columns.item', 'column');
        $classContainer = option('kirby.columns.container', 'columns');

        foreach($columns as $column) {
            $field  = new Field(page(), '', trim($column));
            $html[] = '<div class="' . $classItem . '">' . kirbytext($field) . '</div>';

        return '<div class="' . $classContainer . '">' . implode($html) . '</div>';

      }, $text);

      return $text;


You can simply put this code into /site/plugins/columns/index.php and start using columns in your KirbyText fields. If you want to understand the details, continue reading.

The regular expression voodoo

As the first step to achieve the syntax for the columns, we need to fetch the columns tags:

$text = preg_replace_callback('!\(columns(…|\.{3})\)(.*?)\((…|\.{3})columns\)!is', function($matches) use($text) {
  // do something with the stuff inside the brackets here
}, $text);

Let's examine the regular expression:


The exclamation marks define the beginning and the end of the expression.

is at the end makes sure the expression is case insensitive and the s tells the expression to include new lines.

We could simplify the inner part like this:


The backslashes are there to escape the brackets usually used to group matches, which you can see here: (.*). This little thing translates to: "take everything between the opening columns tag and the closing columns tag and put it in a group".

The final expression contains a bit more complexity to make sure an editor can use either an ellipsis or three dots:

(columns…) or (columns...)

This is achieved with a simple "or" clause, which looks like this:


The \.{3} translates into: "a dot which repeats three times". A dot is another magic character in regular expressions and therefore has to be escaped with the backslash again.

Finally our regular expression fetches what we want and passes the matches to the callback function.

function($matches) use($text) {
  // …

The $matches variable is an array with the following content:

  1. the entire match starting with the beginning columns tag and ending with the closing tag
  2. the first "or" group (… or ...)
  3. the content between the tags
  4. the second "or" group (… or ...)

Since arrays start their index at zero, we can get the content between the tags with $matches[2]

Splitting content into columns

Now that we got the content in between the tags, we can look for our separators and split the content into nice pieces for the columns.

$columns = preg_split('!(\n|\r\n)\+{4}\s+(\n|\r\n)!', $matches[2]);

Since we are regular expression experts now, the expression above isn't that complicated anymore. The only new things are \R, which stands for any line break, and \s, which stands for spaces. So the expression above translates to: "Split the content when there's a line break, followed by four plus signs, follwed by one or more spaces, followed by a line break again."

With this, we get a $columns array with text separated into chunks for our columns.

Nested KirbyText

The column tags are only useful if an editor can use KirbyText and Markdown inside of them. To achieve this, we have to manually parse the content for each column as KirbyText.

A simplified way to do this would be:

$html = [];

foreach($columns as $column) {
  $html[] = '<div class="column">' . kirbytext($column) . '</div>';

But it gets a bit more complex here. KirbyText relies on the related page object in order to get a few things right, such as related images, urls, data, etc. This is why Kirby uses Field objects instead of simple strings for any content that comes from pages and will be parsed with KirbyText. Those Field objects contain the relation to the Page object and make sure that everything is linked correctly.

If we simply call the kirbytext() helper on a regular string like in the example above, the relation to the original Page object would be lost and it would no longer be possible to embed images, which are stored in the content folder of the Page, etc.

That's why we need to wrap the string in a new Field object and connect it to the Page object again:

foreach($columns as $column) {
  $field  = new Field(page(), '', trim($column));
  $html[] = '<div class="column">' . kirbytext($field) . '</div>';

We use the page() helper to fetch the current page and pass it as the first parameter to the new Field object. The second argument is normally used for the field key, but is not needed in this case. As the last parameter we pass the string from the $columns array and trim it to remove any leading or trailing spaces.

The final HTML

We end up with an $html array with entries for each column. The HTML for each column looks like this:

<div class="column">column content</div>

A simple implode function together with a wrapping div makes everything complete.

return '<div class="columns">' . implode($html) . '</div>';

As a last step, we add configurable class names to the div tags, so that you can set different class names in your config.php file, for example:

return [
  'kirby.columns.container' => 'grid', // instead of columns
  'kirby.columns.item' => 'grid-item', // instead of column


To make the columns display correctly as columns, we finally need some CSS, in this example, we use the flexbox model, but you can adapt the markup classes and the styles to fit your needs (use floats, inline-blocks, CSS Grid layout…)

.columns {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  margin-left: -1rem;
  margin-right: -1rem;

.column {
  flex: 0 1 100%;
  margin-left: 1rem;
  margin-right: 1rem;
  max-width: 100%;

@media (min-width: 790px) {
  .column {
    flex: 1;

Once you've added the code above to your CSS file for your site, you should be able to see your grid.