DEV Community

Javier Eguiluz
Javier Eguiluz

Posted on

New in EasyAdmin 5.4: Theming Is Here

EasyAdmin 5.4 is out, and it delivers the feature that we've been building over the past few months: a theming API. You can now recolor and reshape your whole backend with four method calls in your dashboard, without writing any CSS.

This release also brings a redesigned look for tabs, the ability to override each individual part of the detail page, and grouped options in autocomplete association fields. As usual, nothing breaks: your existing backends keep working exactly as before.

Theming Without Writing CSS

By far the most common request I get about EasyAdmin is some variation of "how do I make the backend look like our brand?". Until now the honest answer was: "write CSS overrides". That works, but you have to learn which variables exist, remember to handle the dark color scheme too, and remap entire color palettes by hand.

Here's what a modest rebranding looked like BEFORE, using the design tokens introduced in 5.3:

/* assets/styles/admin.css */
:root {
    --ea-primary: #15803d;
    /* the text/icon color on top of primary-colored elements; you had to pick
       it yourself and get it wrong if your primary color was a light one */
    --ea-primary-foreground: #fff;
    --ea-radius: 0.5rem;
}

/* and now do it again for the dark color scheme... */
.ea-dark-scheme {
    --ea-primary: oklch(0.6 0.2 150);
    --ea-primary-foreground: #fff;
}

/* ...and if you also wanted a warmer neutral scale, remap eleven steps by hand */
:root {
    --gray-50: var(--warm-gray-50);
    --gray-100: var(--warm-gray-100);
    /* ... nine more lines ... */
}
Enter fullscreen mode Exit fullscreen mode

AFTER, in EasyAdmin 5.4, that's a setTheme() call in your dashboard:

use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\Theme;

public function configureDashboard(): Dashboard
{
    return Dashboard::new()
        ->setTitle('ACME Admin')
        ->setTheme(Theme::new()
            ->primaryColor('#15803d', dark: 'oklch(0.6 0.2 150)')
            ->radius('0.5rem')
            ->grays('stone')
        );
}
Enter fullscreen mode Exit fullscreen mode

The design decision here was to not add hundreds of configurable options. The Theme class exposes exactly four options, chosen because each one changes many things at once and coherently:

Theme::new()
    // the accent color of buttons, links, switches, active menu items, etc.
    // Accepts the hexadecimal, rgb(), hsl() and oklch() formats (no alpha channel).
    // The optional 'dark' argument sets a different color for the dark color scheme
    ->primaryColor('#15803d', dark: 'oklch(0.6 0.2 150)')

    // the base border radius that every other radius in the backend derives from.
    // A CSS length in 'px' or 'rem', or a preset: none, xs, sm, md, lg, xl
    ->radius('0.5rem')

    // the base spacing unit that every padding, margin, gap and control height
    // derives from; it defines the density of the whole interface.
    // A CSS length, or a preset: xs, sm, md, lg, xl
    ->spacing('lg')

    // the neutral scale used by all surfaces, borders and text colors:
    // neutral, stone (warmer), zinc, gray or slate (cooler)
    // (values are available as constants too: EasyCorp\Bundle\EasyAdminBundle\Config\Option\GrayScale::ZINC)
    ->grays('zinc', dark: 'stone');
Enter fullscreen mode Exit fullscreen mode

Every option is optional, so call only the ones you care about. A few details worth knowing:

  • The foreground color is computed for you. Picking the text color that sits on top of primary-colored elements is the part everyone gets wrong: white looks great on a dark indigo and terrible on a bright yellow. EasyAdmin derives it automatically from the luminance of your primary color, so a yellow primary gets black text without you thinking about it.
  • md is always the default look. The radius and spacing presets are centered on the current design, so md is a no-op and you can move one step at a time in either direction.
  • spacing() is deliberately a narrow band. The presets only span roughly ±25% around the default, and there's no none. The spacing unit scales every padding, margin, gap and control size linearly, so small changes already resize the entire UI noticeably.

Redesigned Tabs and Other Design Improvements

While building the theming feature, we also fixed and tweaked many design-related elements. For example, fieldsets in forms and detail pages now ensure that all columns in the same row have equal height, which improves the overall design a lot.

Tabs are commonly used to divide complex form and detail pages into smaller sections. We've redesigned them based on how they look in modern design systems. The active tab marker now slides in browsers that support it, and it respects the user's preferences about animations:

The new design of tabs in EasyAdmin backends

You get all of this for free on the new, edit and detail pages. But the tabs are also a new reusable Twig component, in case you want the same look in your own custom pages:

<twig:ea:Tabs>
    <twig:ea:Tabs:Item paneId="general" label="General" active icon="fa fa-gear" />
    <twig:ea:Tabs:Item paneId="seo" label="SEO" />
</twig:ea:Tabs>
Enter fullscreen mode Exit fullscreen mode

Override Any Part of the Detail Page

The contents of the detail page used to be rendered by a set of Twig macros defined inside crud/detail.html.twig. Macros aren't overridable, so if you wanted to change one small thing (say, how fieldsets render) your only option was to copy the entire template into your app, and then inherit the maintenance burden of every upstream change to it.

This is now fixed: those macros have been extracted into thirteen standalone templates that you can replace one at a time:

  • crud/detail/field_group: the label and value of each regular field;
  • crud/detail/layout_field: decides which of the templates below renders each form layout field;
  • crud/detail/tab_list: the clickable list of tab names;
  • crud/detail/tab_group_open and crud/detail/tab_group_close: the element wrapping all tab panes;
  • crud/detail/tab_open and crud/detail/tab_close: each tab pane;
  • crud/detail/column_group_open and crud/detail/column_group_close: the element wrapping all columns;
  • crud/detail/column_open and crud/detail/column_close: each column;
  • crud/detail/fieldset_open and crud/detail/fieldset_close: each fieldset.

Use them like any other EasyAdmin template name:

public function configureCrud(Crud $crud): Crud
{
    return $crud
        ->overrideTemplate('crud/detail/fieldset_open', 'admin/detail/fieldset_open.html.twig')
    ;
}
Enter fullscreen mode Exit fullscreen mode

Or rely on Symfony's mechanism to override bundle templates, by creating templates/bundles/EasyAdminBundle/crud/detail/fieldset_open.html.twig.

Existing applications are unaffected, whether they copied the whole detail.html.twig template or extended it with @!EasyAdmin/crud/detail.html.twig. The only case that needs a change is the unlikely one where you imported those macros directly with {% from '@EasyAdmin/crud/detail.html.twig' import ... %}; see UPGRADE.md for the details.

Grouped Options in Autocomplete Association Fields

Autocomplete association fields now support the group_by option, which completes the work started when choice_label support was added. If your related entities naturally fall into categories, you can group them in the dropdown:

use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;

yield AssociationField::new('issues')
    ->autocomplete()
    ->setFormTypeOption('group_by', static fn (Issue $issue): ?string => $issue->getProject()?->getName())
;
Enter fullscreen mode Exit fullscreen mode

It also accepts a property path string, exactly like Symfony's EntityType option:

yield AssociationField::new('issues')
    ->autocomplete()
    ->setFormTypeOption('group_by', 'project.name')
;
Enter fullscreen mode Exit fullscreen mode

The semantics match EntityType's group_by in every way: it takes a callable, a property path string or a PropertyPath object, and it fails gracefully. Returning null or pointing at an unreadable property path simply leaves that entry ungrouped instead of throwing. Grouping works for both the already-selected values (rendered server-side as <optgroup> elements) and the results loaded on demand via Ajax.

Summary

EasyAdmin 5.4 brings a theming API that re-themes the entire backend from PHP with four options and no CSS; redesigned tabs following modern design systems, available as a reusable Twig component; overridable detail page templates, so you never have to copy the whole template again; and grouped options in autocomplete association fields. All of it 100% backwards compatible.


✨ If EasyAdmin helps your company ship faster, consider funding its development 🙌💡

Top comments (0)