ūüöÄ A new era: Kirby 4 Get to know
Skip to content

Kirby 4

A new era

New design

Not just some fresh paint

Page creation dialogs

Faster, better page creation

Customize the page creation dialog to your needs.

Give the title a fitting label, add new fields or disable redirecting to the new page. Did we mention you can also define the initial page status after creation now?

Read more …

title: Product
    label: Product name
    - price
    - brand
  redirect: false
  status: listed

Moving pages

I like to move it, move it

A new parent for the page can be picked with our brand new page tree dialog

Created a page in the wrong place or changed your mind? Just move it to a new parent!

Read more …

The page dropdowns have been extended with the new Move page option

Section filters

Filter pages and files

An example for a shop with filtered products by price range

Filter pages and files by any criteria: The pages and files sections now come with support for our powerful query string syntax.

Read more …

  extends: sections/products
  label: Expensive
  query: page.childrenAndDrafts.filter('price', '>', '99')
  extends: sections/products
  label: Cheap
  query: page.childrenAndDrafts.filter('price', '<=', '99')

Color field

Color me surprised

Color in all its facets: Multiple color notations, color picker, pre-defined colors, custom color names and transparency. You can't get more color than that.

Read more …

The new color field with the color picker dropdown and predefined colors with names
  type: color
    "Sunny rays": "#F8B195"
    "First-love blush": "#F67280"
    "Cherry blossom": "#C06C84"
    "Morning gloom": "#6C5B7B"
    "Midnight rain": "#355C7D"
The color field can be simplified to show a set of predefined colors instead of offering a color picker

Image focus

Focus Pocus

The focus point for images can now be set directly in the file view. Drop a marker on the most relevant point in the image to always crop around a custom center point.

Never cut off the most important part of your images again. Set a focus point and let your images shine in all their beauty.

Read more …


Uploading files the smart way

The new upload dialog with four images, ready to be uploaded. Each file has a new input to change the filename before the file gets uploaded.

Screenshot-123.jpg? With the preview and edit options in the new upload dialog, meaningless file names are now a thing of the past. Use the new blueprint options to optimize uploads before they land on the server.

Read more …

title: Image

# Optimize on upload
  width: 500
  height: 500
  crop: true

Block field improvements

Our best field is now 25% bester

The new field preview for blocks lets you edit block content directly in form fields instead of a live block preview
    type: blocks
        wysiwyg: true
        preview: fields
                type: text
                type: text
                type: writer
            label: Settings
            fields: # ...

Display and edit headings level inline

The heading level is now displayed right next to the heading and can be used to change the level on the fly

New toggles inside the drawer

The heading level can also be set with the new toggles in the block drawer

Block splitting

  • Split and merge text, list and headings
  • New option buttons to split or merge
  • Press enter at the end of a headline to append a new text block
  • Text block with¬†inline: true¬†for text field will split directly on Enter (Shift + Enter creates a hard line break)
  • Custom blocks can support splitting by implementing a¬†split¬†method

Read more …

Minimized blocks while dragging

Blocks are now minimized while you drag them around. This makes it easier to sort blocks with long content.

New Keyboard shortcuts

Action Shortcut
Remove block Meta + Backspace
Move focus up Meta + ↑
Move focus down Meta + ↓
Move block up Meta + Shift + ↑
Move block down Meta + Shift + ↓
Extend selection up Meta + Alt + ↑
Extend selection down Meta + Alt + ↓
Split block Meta + Enter
Merge block Meta + J

Layout field improvements

Copy & Change

More flexibility: Easily change a layout, and copy one or all layouts into other layout fields.

Read more …

New copy, paste and change layout options in the layout dropdown

Time-based, one-time passwords

Making your Panel even more secure

Kirby now supports time-based one-time codes (TOTP), offering more secure ways to sign into the Panel. When two-factor authentication is activated, one-time codes from your authenticator app will be used instead of codes via email.

Each user can set up TOTP in the account view in the Panel, adding their secret key to their authenticator app.

Read more …

Time-based one-time passwords in Kirby 4
return [
    'auth' => [
        'methods' => [
            'password' => ['2fa' => true]

Language editor

No longer lost in translation

The new language view with general information about the language and the new language variable editor.

Each language got its own view now. Easily configure your languages or edit language variables right from the Panel. Easy, right?

Read more …

Search view

Deliver results

The new search view with a long list of results for a file search.

The larger your website, the more important becomes search. With Kirby's new search view, searching in the Panel has become a breeze.

Dropdown with all available link types (URL, Page, File, Email and Phone Number)

Changeable file templates

Change is in the air

The new dialog to change the template of a file.

Change file templates to any allowed template defined by its parent in a files section or field.

Read more …

Writer field improvements

Writing in style

With custom marks and nodes for your own style, customizable heading levels and a character counter, you have full control about your writing.

Read more …

The updated writer field with the new toolbar option that resembles the fixed toolbar of the textarea
The link picker for the writer now also features the new link field

Link to internal pages & files with our new link field in the link dialog. No more guessing of URLs.

Toolbar position + layout
    type: writer
      inline: false
        - bold
        - "|"
        - link
    type: writer
      - 2
      - 3
      - 4
New marks
    type: writer
      - sub
      - sup
      - clear
    type: writer
    minlength: 10
    maxlength: 360
    counter: false

Writer Plugins

You need a block or inline format that’s not available? Create your own marks and nodes with our brand new extension API for Writer plugins.

Read more …

panel.plugin("acme/writer", {
  writerNodes: {
    blockquote: {
      // ...
  writerMarks: {
    mention: {
      // ...

Custom textarea buttons

Bring your own

Missing that one special button for your custom text markup? Make one.

Read more …

panel.plugin("acme/textarea", {
  textareaButtons: {
    highlight: {
      label: "Highlight",
      icon: "wand",
      click: () => this.command("toggle", "<mark>", "</mark>")
      shortcut: "m"

Srcset in image tag

Image tag goes responsive

The image KirbyTag has turned responsive. The srcset attribute supports manual settings and presets.

Read more …

With absolute sizes

(image: alanis-moresrcset.jpg srcset: 200, 300)

With predefined srcset

(image: alanis-moresrcset.jpg srcset: artist)

Default page model

Set a new page standard


use Kirby\Cms\Page;

class DefaultPage extends Page
     * This method is now available for all pages
     * unless they have their own page model.
    public function myCustomMethod(): string
        return 'Hello world';

The default page model kicks in when there is no specific model for a page.

Of course, other models can extend the default page model, too.

Read more …

Panel JavaScript API

Remote control

Our new Panel JavaScript API gives you access to the most important Panel features. Control dialogs, drawers, notifications and more from your plugins, your custom panel.js or even the console.

Read more …

All major Panel features can now be controlled directly from your browser console

PHP 8.3

PHP moves forward, we do too

New Versioning Scheme

Semantic Versioning

{generation} {major} {minor} {patch}
3 9 0 0
3 10 0 0
3 11 0 0
{generation} {major} {minor} {patch}
3 3 9 0
4 0 0
5 0 0

Key points

Example roadmap

  • 4.0.0: late 2023
  • 5.0.0: late 2024
  • 6.0.0: late 2025



Core improvements

QR Code generation

Kirby's new Kirby\Image\QrCode class, qr() helper function and ->toQrCode() field method allow you to create QR Codes right out of the box:

$qr = new Kirby\Image\QrCode('https://getkirby.com');
$qr->toSvg(color: '#ff00ff');
$qr->toDataUri(color: '#ff00ff');


Support for UUIDs and permalinks in URL helper

You can now pass a page UUID, a file UUID or a permalink to the url() helper and it will be converted to the actual URL:

<?= url('page://abcd') ?>
<?= url('file://abcd') ?>
<?= url('/@/page/abcd') ?>
<?= url('/@/file/abcd') ?>

This will also work for other URL methods like $field->toUrl(), Url:to(), etc.

Support for UUIDs in API calls

You can now use UUIDs in API calls to request pages and files. #4769


The UUIDs must be passed without scheme, but a prefixed @. I.e.:

// Nope

// Yes

File routes can also handle file UUIDs for files and parents:


In addition to that there are new direct UUID file routes:


Better plugin asset handling

  • New assets plugin extension that allows plugins to specify assets from custom paths and with a wider range of extensions than previously supported
  • New Kirby\Cms\Plugin::assets() and Kirby\Cms\Plugin::asset($assetName) methods
  • New Kirby\Cms\PluginAsset object with many methods, e.g. $plugin->asset('styles.css')->url()
  • Plugin asset media url contains a modification timestamp to easily cachebust (e.g. /media/plugins/getkirby/test-plugin/2375797551-472389240/styles.css)
  • css() and js() helpers support passing plugin and plugin assets objects to include all assets of the plugin:



New SymmetricCrypto class

Kirby\Toolkit\SymmetricCrypto is a user-friendly and safe abstraction for symmetrical authenticated encryption using the PHP sodium extension:

use Kirby\Toolkit\SymmetricCrypto;

// encryption/decryption with a password
$crypto     = new SymmetricCrypto(password: 'super secure');
$ciphertext = $crypto->encrypt('a very confidential string');
$plaintext  = $crypto->decrypt($ciphertext);

// encryption with a random key
$crypto     = new SymmetricCrypto();
$ciphertext = $crypto->encrypt('a very confidential string');
$secretKey  = $crypto->secretKey();

// encryption/decryption with a previously generated key
$crypto     = new SymmetricCrypto(secretKey: $secretKey);
$ciphertext = $crypto->encrypt('a very confidential string');
$plaintext  = $crypto->decrypt($ciphertext);

New LazyValue class

Kirby\Toolkit\LazyValue allows to protect a closure function from being called by other code that is testing for instanceof Closure:

$value = new LazyValue(fn ($a, $b, $c) => 'please protect me');

// ...

if ($value instanceof Closure) {
  // we would hate if our value gets unwrapped here,
  // that's why we cannot use a normal closure but LazyValue
  $value = $value();

// ...

// finally we want to unwrap our protected closure
if ($value instanceof LazyValue) {
  $value = $value->resolve($a, $b, $c);

More improvements

Panel improvements

Dialogs for fields

Fields can now define their own dialogs on the backend:

  • Fields with array definition:
    return [
      'props' => [
        // ...
      'dialogs' => function () {
        return [
            'pattern' => 'delete',
            'load' => function () {},
            'submit' => function () {},

  • Field classes:
    class MyField extends FieldClass
      public function dialogs(): array
        return [
            'pattern' => 'delete',
            'load' => function () {},
            'submit' => function () {},

The route patterns for field dialogs are automatically prefixed with the following scheme:


Here's an example:


In a field component, the dialog can be opened by using the field endpoint:

this.$dialog(this.endpoints.field + "/delete")

Customization of the layout selector

The layout selector is now customizable with the size (small, medium, large, huge) and columns options via the new selector prop:

  type: layout
    # `small`, `medium`, `large` or `huge`
    size: huge
    columns: 6

More improvements

  • Disabled buttons can receive focus (tab) which allows them to be read out by screenreaders.
  • Structure fields don‚Äôt disregard content changes anymore when clicking outside the form (now drawer).
  • Correct autofocus handling for blocks, layout, structure, picker fields and dropdowns
  • Improved focus styles for links and the flag preview in tables
  • All minified panel assets now add .min to the filename. This will avoid auto-minification in Cloudflare and possibly other environments.
  • Improved text overflow behavior for links in tables
  • Increase the font size for help text in sections and fields
  • The multiselect and tag dropdowns now offer more space to not cut off longer options.
  • Slug field: respect custom slug allowlist from the Str class
  • Pages section: new templatesIgnore option #5322
  • Users field: set default: true to always use the currently logged in user as default
  • New items size full

New Icons

We've added new icons to our Panel iconset: anchor, clear, lab, merge and split.

New Components

  • <k-alpha-input>
  • <k-browser>
  • <k-code>
  • <k-color-frame>
  • <k-coloroptions-input>
  • <k-colorname-input>
  • <k-dialog-body>
  • <k-dialog-box>
  • <k-dialog-buttons>
  • <k-dialog-fields>
  • <k-dialog-footer>
  • <k-dialog-form>
  • <k-dialog-notification>
  • <k-dialog-search>
  • <k-dialog-text>
  • <k-drawer-body>
  • <k-drawer-fields>
  • <k-drawer-header>
  • <k-drawer-notification>
  • <k-drawer-tabs>
  • <k-file-browser>
  • <k-frame>, <k-icon-frame> and <k-image-frame>
  • <k-hue-input>
  • <k-navigate>
  • <k-notification>
  • <k-page-tree>
  • <k-search-input>
  • <k-stat>
  • <k-text-drawer>
  • <k-tree>

New helpers

  • $helper.focus(element)
  • $helper.object.length(object)
  • $helper.string.isEmpty(string)
  • $helper.url.base
  • $helper.url.buildQuery
  • $helper.url.buildUrl
  • $helper.url.isAbsolute
  • $helper.url.isSameOrigin
  • $helper.url.isUrl
  • $helper.url.makeAbsolute
  • $helper.url.toObject
  • $library.colors

New JS error classes

  • RequestError
  • JsonRequestError

And so much more

We've tweaked the Panel extensively with too many improvements to name them all. But here are some more:

  • Load Panel area views dynamically with the new when prop
  • Each Panel area can now define additional requests for simple data endpoints or actions
  • Syntax highlighting in the k-code with Prism
  • Text fields: new font: monospace option
  • panel.css and panel.js config options now also support arrays with multiple entries as well as absolute URLs
  • k-pagination can always be navigated by keys (no extra prop needed anymore)
  • Notifications: support for custom icons
  • k-tag supports an image/icon frame
  • New disabled property for the k-options-dropdown component
  • New selected property for k-button to set the aria-selected attribute

Bug fixes

  • Removed flickering from¬†k-pagination¬†when navigating
  • Fixed name of default blueprints to¬†pages/default and files/default
  • Blocks: batch selection allows deselecting blocks
  • Blocks: batch selection gets deselected on Escape key
  • Fixed i18n translate issues, e.g. for the user blueprint title¬†#4869
  • Writer field: Email mark toolbar button title is properly translated now
  • Empty required Writer field now shows proper invalid styling in Panel
  • UUIDs are less often generated when not needed to be generated
  • Kirby queries now differentiate between integers and floats as arguments
  • Kirby no longer hides errors in the response class when the response is converted to a string¬†#5027
  • Files field upload: use upload.parent for mime check #5245
  • $field->toUrl() will return null on empty fields #5259
  • Users field does not use a default anymore if none set
  • The "Session ... is currently read-only because it was accessed via an old session" error is circumvented when the PHP sodium extension is available
  • Removed the error boundary from <k-fieldset>; the error boundary kills the entire field/input if an error occurs, which is way too aggressive and also makes it more difficult to handle errors properly.
  • App::multilang() and App::defaultLanguage() are correctly updated on changes to language data #5342
  • Prevent unnecessary runs in Language::update() when updating the default language
  • Block title: certain HTML characters are now properly unescaped in the label #5346




  • More frontend unit tests
  • Cleaned up¬†k-toolbar¬†component
  • New drawer component structure:
    <k-overlay type="drawer">
      <form class="k-drawer" method="dialog">
        <k-drawer-notification />
        <k-drawer-header />
          <k-drawer-fields />
  • Portals: Dialogs, Drawers and other Overlays are now separated into different portals. This is done by setting the overlay type:
    • <k-overlay type="dialog">
    • <k-overlay type="drawer">
    • <k-overlay>
      An overlay without a type will use the default overlay portal. Separating them into different portals gives us more control over z-index and layering of elements.
  • Removed¬†v-model¬†from¬†k-form¬†and¬†k-fieldset
  • Removed $listeners usage from k-draggable, k-button¬†and subcomponents, k-link, k-headline, k-form, k-block, k-block-title, k-box, k-image, k-content-item
  • Made k-bubble more flexible
  • Use the Vue object syntax for dynamic :style attributes to enhance the robustness and security
  • Reduce JS forEach usage



  • Files in a plugin's assets directory are now always assumed to be public, independent of their file extension. If your plugin needs to store other files in the assets directory, please use the new assets extension to explicitly define the public assets.
  • Passing a single space as value to¬†Xml::attr()¬†(with the intention to generate an attribute with an empty value) has been deprecated in favor of passing an empty string.
  • Kirby\Cms\Model: Use Kirby\Cms\ModelWithContent instead
  • Kirby\Email\Email::clone() and¬†Kirby\Email\Body::clone()
  • Page::isReadable(): Use Page::isAccessible() instead
  • The name query should not be used for custom API endpoints anymore, it will be used for the Kirby QL (KQL) plugin/core implementation
  • Internal $model->contentFile(), $model->contentFiles(), $model->contentFileDirectory(), $model->contentFileExtension() and $model->contentFileName() methods have been deprecated and will be removed in v5.


  • Custom icons using a 16x16 viewbox have been deprecated. In an upcoming version, Kirby will only support custom icons with a 24x24 viewbox by default. If you want to continue using icons with a different viewport, please wrap them in an <svg> element with the corresponding viewBox attribute.
  • Icons circle-outline, heart-outline and star-outline: Use circle, heart and star instead
  • this.$events: Use¬†this.$panel.events¬†instead
  • this.$panel.events.$on: Use¬†this.$panel.events.on¬†instead
  • this.$panel.events.$off: Use¬†this.$panel.events.off¬†instead
  • this.$panel.events.$emit: Use¬†this.$panel.events.emit¬†instead
  • this.$store.dispatch("isLoading"): Use this.$panel.isLoading¬†instead
  • this.$translation: Use¬†this.$panel.translation¬†instead
  • this.$store.dispatch("dialog"): Use¬†this.$panel.dialog.open¬†and¬†this.$panel.dialog.close¬†instead
  • this.$store.dispatch("drag", drag): Use¬†this.$panel.drag = drag¬†instead
  • k-dropdown: Use k-dropdown-content as standalone instead
  • k-calendar was renamed to k-calendar-input; k-calendar is still available but only as deprecated alias.

Breaking changes


  • Kirby 4 requires at least PHP 8.1 and supports up to PHP 8.3.
  • When impersonating the almighty kirby user, any permission check will succeed even if permission has been disabled for regular admins.
  • The twitter icon, KirbyTag and helper have been removed. Use the legacy plugin if you still rely on these: legacy-twitter.zip
  • We have added more native PHP type hints throughout the system. When extending core classes, this might require you to update your method to include those type hints as well.
  • Users field doesn't automatically use the current user as default, add default: true to keep this functionality.
  • When thumb generation fails, the image API now throws an error and no longer loads the original image.
  • Files: manipulate is now a core method and focus a reserved field by the core. These names are no longer available for custom file methods. Previous content fields with these names can only be accessed via e.g. $file->content()->get('manipulate').
  • I18n::translate(): If $fallback is an array and neither the array $key nor the array $fallback have a matching entry for the locale, the first element of the $key array will now be returned (not anymore the one from $fallback). If $fallback is a string, it will be considered with priority over both of these.
  • If overwriting the¬†hidden¬†field, it must return¬†'hidden' => true¬†now.
  • Removed deprecated DS constant. Use / instead.
  • When sanitizing DOM objects (e.g. in the writer field, but not during the sanitization/validation of uploaded files), host-relative URLs that point outside the site root are now allowed as the use of the HTML <base> element is assumed for sites in a subfolder. To revert to the old, strict behavior, set Sane's allowHostRelativeUrls option to false.
  • Renamed parameter of ::group() method of all collection classes to $caseInsensitive
  • Errors are no longer hidden when a Response is converted to a string.
  • The $translations->start() and $translations->stop() methods were no longer in use and have been removed. Content files are automatically converted by the Language class.
  • $languages->codes() now returns ['default'] for single language installations.
  • Kirby\Panel\Assets::custom() now returns an array.
  • Kirby\Uuid\Uuid¬†is now an abstract class, child classes need to implement the¬†id¬†method.
  • Kirby\Uuid\Uuid::key()¬†can now also return¬†null¬†if the new¬†$generate¬†parameter isn't passed as¬†true¬†and no UUID has yet been generated for the model.
  • Kirby\Cms\ModelWithContent¬†doesn't extend¬†Kirby\Cms\Model¬†anymore.
  • Removed the ::clone() method from Kirby\Cms\Auth\Status, Kirby\Http\Uri, Kirby\Cms\FileVersion, Kirby\Filesystem\Asset, Kirby\Filesystem\File, Kirby\Image\Image, Kirby\Cms\Plugin, Kirby\Cms\Role, Kirby\Cms\StructureObject and Kirby\Cms\ContentTranslation classes
  • Removed the ::hardcopy() method from Kirby\Api\Api, Kirby\Cms\Api,Kirby\Email\Email, Kirby\Email\Body, Kirby\Cms\Auth\Status, Kirby\Toolkit\Pagination, Kirby\Http\Uri, Kirby\Cms\FileVersion, Kirby\Filesystem\Asset, Kirby\Filesystem\File, Kirby\Image\Image, Kirby\Cms\Role, Kirby\Cms\StructureObject and Kirby\Cms\ContentTranslation classes
  • Removed ::site() method from Kirby\Cms\Plugin, Kirby\Cms\Role and Kirby\Cms\StructureObject classes
  • Removed ::kirby() method from Kirby\Cms\Role and Kirby\Cms\StructureObject classes
  • Unauthenticated API and Panel calls now return a correct 401 HTTP code instead of 403.
  • Registering a default block model now needs to be done with the key default, not Kirby\Cms\Block.
  • new Kirby\Cms\Structure()/new Kirby\Cms\StructureObject() don't work anymore as before. Use Kirby\Cms\Structure::factory()/Kirby\Cms\StructureObject::factory() instead.
  • Kirby\Cms\Items::factory() and all inheriting classes throw an exception now if malformed data is passed.
  • Extending the internal $model->contentFile(), $model->contentFiles(), $model->contentFileDirectory(), $model->contentFileExtension(), $model->contentFileName(), $model->readContent() and $model->writeContent() methods in a page model will no longer have an effect as these methods are no longer called by the core. Please extend the new Kirby\Content\PlainTextContentStorageHandler class instead and return an instance of your custom class from $model->storage(). Please note that the interface of PlainTextContentStorageHandler is internal and may change in the future.
  • New $isExternal argument for the Kirby\Sane\Handler::sanitize() and Kirby\Sane\Handler::validate() methods that custom Sane handlers need to implement; it allows to differentiate between strings from external files that may be accessed directly and strings that will end up directly on the page.


  • Blocks: Removed keyboard shortcut to move block focus up/down
  • Removed road-sign icon
  • The icons circle, heart and star are now named circle-filled, heart-filled and star-filled.
  • Removed this.$config.search from Panel
  • Area search plugins receive two additional arguments for their query callback: $limit and $page to be used to paginate the results. They should then return an array with entries results and pagination. #5191
  • <k-header> doesn't include tabs anymore by default. Use <k-tabs> separately.
  • Defining the footer slot in <k-dialog> will no longer wrap the slot content in the <footer> element. This can now be more flexibly handled by using <k-dialog-footer> inside the slot.
  • The form drawer no longer automatically closes on submit. This is introducing the same behaviour as in dialogs. Auto-closing might often not be the intended result of submitting the form and it's easier to close it manually in a submit handler than to re-open it again.
  • this.$store.state.isLoading¬†is no longer available. You can now use¬†window.panel.isLoading¬†or¬†this.$panel.isLoading¬†in Vue components to access the current loading state.
  • this.$store.state.dialog¬†is no longer available. Use¬†this.$panel.dialog¬†instead.
  • Removed¬†this.$store.state.drag, use¬†window.panel.drag/this.$panel.drag¬†instead.
  • <k-button>¬†,¬†<k-link>,¬†<k-headline> and <k-content-item> only emit the¬†click¬†event. For other native events, use the¬†.native¬†event listener modifier.
  • Native events (e.g.¬†click,¬†dbclick) need the¬†.native¬†modifier now when used on¬†<k-block>¬†and¬†<k-block-title>
  • Need to use¬†.native¬†modifier for all previous event listeners on¬†<k-box and¬†<k-image>
  • <k-pagination> doesn't support setting custom labels/titles via nextLabel, prevLabel or pageLabel
  • <k-range was removed and replaced by <k-alpha-range> and <k-hue-range>.
  • <k-choice> has been removed. Use <k-choice-input> instead.
  • The unused theme prop has been removed from <k-choice-input>.

Removed deprecated code

QR Code¬ģ is a registered trademark of DENSO WAVE INCORPORATED.

Migration from previous versions

Kirby 4 is a major release and requires some changes to existing sites. To ease the transition, we have compiled everything you need to know in a handy guide:

Migration guide ‚Üí

If you are not updating directly from Kirby 3.9 but an older version, please first perform each major update step by step. Please refer to the changelogs of Kirby 3.5, Kirby 3.6, Kirby 3.7, Kirby 3.8 and Kirby 3.9 for details.

Get started