{"id":98023,"date":"2026-10-06T07:50:00","date_gmt":"2026-10-06T11:50:00","guid":{"rendered":"https:\/\/overcentral.com\/en\/?p=98023"},"modified":"2026-09-29T07:55:14","modified_gmt":"2026-09-29T11:55:14","slug":"customize-emdash-admin-dashboard-98023","status":"publish","type":"post","link":"https:\/\/overcentral.com\/en\/customize-emdash-admin-dashboard-98023\/","title":{"rendered":"How to Customize the EmDash Admin Dashboard for Your Brand"},"content":{"rendered":"<p>The standard advice you\u2019ll hear everywhere? \u201cJust drop a custom CSS snippet into the admin theme.\u201d It\u2019s clean, it\u2019s simple, and it works\u2014until it doesn\u2019t. Most tutorials stop there. But if you\u2019re running a multi-author site, an agency with ten client instances, or a brand with strict accessibility requirements, that one-liner leaves you exposed. CSS overrides are fragile. One update to EmDash\u2019s core templates and your delicate overrides snap. The admin panel\u2019s markup isn\u2019t documented for public consumption, so you\u2019re essentially patching blind.<\/p>\n<p>This article walks you through a more robust, future-proof approach: building a custom admin theme plugin for <a href=\"https:\/\/emdash.dev\" target=\"_blank\" rel=\"noopener noreferrer\" data-iacss-external=\"1\">EmDash<\/a> that persists across updates, respects Cloudflare\u2019s security model, and actually scales. You\u2019ll learn why the CSS-only path is a trap, and how to use EmDash\u2019s plugin system and Astro\u2019s theming capabilities to take full control of the dashboard\u2014without forking the core.<\/p>\n<h2>Why \u201cJust Add CSS\u201d Is the Wrong Starting Point<\/h2>\n<p>Everyone says it: \u201cGo to your EmDash theme\u2019s <code>src\/styles\/<\/code>codecodecodecode folder, drop your brand colors into the admin CSS file, and you\u2019re done.\u201d That works for a single instance on version 0.1.0. But here\u2019s what that advice glosses over:<\/p>\n<ul>\n<li><strong>EmDash updates can rewrite admin styles.<\/strong> The <code>@astrojs\/mdash<\/code>codecodecodecode package ships default dashboard CSS. When Cloudflare pushes a patch, your overrides might get overwritten or break selectors that changed. You won\u2019t know until a writer clicks \u201cSave\u201d and sees a broken layout.<\/li>\n<li><strong>No isolation between your custom styles and plugin styles.<\/strong> A plugin that injects its own CSS can accidentally clash with yours. Without scoping, you\u2019re debugging with DevTools every other week.<\/li>\n<li><strong>Multi-instance maintenance becomes a nightmare.<\/strong> If you manage five client sites, each with its own brand, you\u2019re now maintaining five fragile CSS forks. One global update to EmDash means five manual merge checks.<\/li>\n<\/ul>\n<p>The better way? Build a <strong>brand-plugin<\/strong> that uses EmDash\u2019s plugin sandbox to inject scoped, versioned admin styles. This leverages the same dynamic worker architecture that makes EmDash secure in the first place\u2014your brand customization becomes a first-class, isolated module.<\/p>\n<h2>What You\u2019ll Need Before Starting<\/h2>\n<ul>\n<li>A running EmDash instance (local or <a href=\"https:\/\/workers.cloudflare.com\" target=\"_blank\" rel=\"noopener noreferrer\" data-iacss-external=\"1\">Cloudflare Workers<\/a>). If you haven\u2019t set one up, run <code>npm create emdash-latest<\/code>codecodecodecode and follow the wizard.<\/li>\n<li>Basic knowledge of TypeScript and <a href=\"https:\/\/astro.build\" target=\"_blank\" rel=\"noopener noreferrer\" data-iacss-external=\"1\">Astro<\/a> components.<\/li>\n<li>EmDash version 0.1.0 or later (check with <code>npx emdash version<\/code>codecodecodecode).<\/li>\n<li>A Cloudflare Workers Paid plan if you want sandboxed plugin execution (the free tier runs plugins in-process, which is fine for development).<\/li>\n<\/ul>\n<h2>Step-by-Step: Building a Branded Admin Theme Plugin<\/h2>\n<p>We\u2019ll create a plugin named <code>admin-brand<\/code>codecodecodecode that overrides the admin dashboard\u2019s colors, logo, and typography\u2014without touching EmDash\u2019s core files.<\/p>\n<h3>1. Structure the Plugin Directory<\/h3>\n<p>Inside your EmDash project, create a folder at <code>plugins\/admin-brand\/<\/code>codecodecodecode with this layout:<\/p>\n<p>&#8220;`<\/p>\n<p>plugins\/admin-brand\/<\/p>\n<p>\u251c\u2500\u2500 manifest.json<\/p>\n<p>\u251c\u2500\u2500 hooks.ts<\/p>\n<p>\u251c\u2500\u2500 styles\/<\/p>\n<p>\u2502   \u251c\u2500\u2500 base.css<\/p>\n<p>\u2502   \u2514\u2500\u2500 components.css<\/p>\n<p>\u2514\u2500\u2500 assets\/<\/p>\n<p>    \u2514\u2500\u2500 logo.svg<\/p>\n<p>&#8220;`<\/p>\n<p>The <code>manifest.json<\/code>codecodecodecode file declares the plugin\u2019s identity and capabilities:<\/p>\n<p>&#8220;`json<\/p>\n<p>{<\/p>\n<p>  &#8220;id&#8221;: &#8220;admin-brand&#8221;,<\/p>\n<p>  &#8220;version&#8221;: &#8220;1.0.0&#8221;,<\/p>\n<p>  &#8220;capabilities&#8221;: [&#8220;read:admin&#8221;, &#8220;write:admin&#8221;]<\/p>\n<p>}<\/p>\n<p>&#8220;`<\/p>\n<p>The <code>hooks.ts<\/code>codecodecodecode file is where you\u2019ll inject your custom CSS. EmDash\u2019s plugin API exposes a hook called <code>admin:afterRender<\/code>codecodecodecode that runs after the admin dashboard HTML is generated. You\u2019ll use it to append a <code><\/code>codecodecodecode tag with your brand styles.<\/p>\n<h3>2. Write the Hook<\/h3>\n<p>In <code>hooks.ts<\/code>codecodecodecode, define the plugin\u2019s logic:<\/p>\n<p>&#8220;`typescript<\/p>\n<p>import { definePlugin } from &#8216;@astrojs\/mdash\/plugin&#8217;;<\/p>\n<p>export default definePlugin({<\/p>\n<p>  id: &#8216;admin-brand&#8217;,<\/p>\n<p>  hooks: {<\/p>\n<p>    &#8216;admin:afterRender&#8217;: async ({ context: { adminHtml } }) =&gt; {<\/p>\n<p>      \/\/ Read the CSS files bundled with the plugin<\/p>\n<p>      const baseCSS = await fs.readFile(&#8216;.\/styles\/base.css&#8217;, &#8216;utf-8&#8217;);<\/p>\n<p>      const componentsCSS = await fs.readFile(&#8216;.\/styles\/components.css&#8217;, &#8216;utf-8&#8217;);<\/p>\n<p>      \/\/ Inject a scoped  into the admin head<\/p>\n<p>      const brandedHtml = adminHtml.replace(<\/p>\n<p>        &#8221;,<\/p>\n<p>        <code>${baseCSS}n${componentsCSS}<\/code>codecodecodecode<\/p>\n<p>      );<\/p>\n<p>      return brandedHtml;<\/p>\n<p>    },<\/p>\n<p>  },<\/p>\n<p>});<\/p>\n<p>&#8220;`<\/p>\n<p>This approach ensures your styles are injected <em>after<\/em> EmDash\u2019s own CSS, so you can override properties without <code>!important<\/code>codecodecodecode (most of the time). The <code>id<\/code>codecodecodecode attribute makes it easy to target or remove the styles later.<\/p>\n<h3>3. Write the CSS Files<\/h3>\n<p>Create <code>styles\/base.css<\/code>codecodecodecode with global overrides:<\/p>\n<p>&#8220;`css<\/p>\n<p>:root {<\/p>\n<p>  &#8211;mdash-admin-bg: #f8f9fa;<\/p>\n<p>  &#8211;mdash-admin-text: #212529;<\/p>\n<p>  &#8211;mdash-admin-primary: #0d6efd;<\/p>\n<p>  &#8211;mdash-admin-secondary: #6c757d;<\/p>\n<p>}<\/p>\n<p>body.admin {<\/p>\n<p>  font-family: &#8216;Inter&#8217;, system-ui, sans-serif;<\/p>\n<p>}<\/p>\n<p>&#8220;`<\/p>\n<p>And <code>styles\/components.css<\/code>codecodecodecode for specific UI elements:<\/p>\n<p>&#8220;`css<\/p>\n<p>\/<em> Navigation sidebar <\/em>\/<\/p>\n<p>.mdash-sidebar {<\/p>\n<p>  background: var(&#8211;mdash-admin-primary);<\/p>\n<p>  color: white;<\/p>\n<p>}<\/p>\n<p>.mdash-sidebar a {<\/p>\n<p>  color: rgba(255, 255, 255, 0.85);<\/p>\n<p>}<\/p>\n<p>\/<em> Content area headers <\/em>\/<\/p>\n<p>.mdash-content-header {<\/p>\n<p>  border-bottom: 2px solid var(&#8211;mdash-admin-secondary);<\/p>\n<p>}<\/p>\n<p>&#8220;`<\/p>\n<p>The class names (<code>mdash-sidebar<\/code>codecodecodecode, <code>mdash-content-header<\/code>codecodecodecode) are not guaranteed to stay stable across EmDash updates, but because <em>you control the plugin<\/em>, you can update the selectors in one place when the upstream markup changes. That\u2019s infinitely better than hunting through a forked CSS file.<\/p>\n<h3>4. Add a Custom Logo<\/h3>\n<p>Place your brand logo in <code>assets\/logo.svg<\/code>codecodecodecode. Then, in <code>hooks.ts<\/code>codecodecodecode, also replace the default logo:<\/p>\n<p>&#8220;`typescript<\/p>\n<p>const logo = await fs.readFile(&#8216;.\/assets\/logo.svg&#8217;, &#8216;utf-8&#8217;);<\/p>\n<p>const brandedHtml = brandedHtml.replace(<\/p>\n<p>  &#8216;<\/p>\n<div class=\"mdash-logo\">&#8216;,<\/p>\n<p>  <code><\/p>\n<div class=\"mdash-logo\">${logo}<\/code>codecodecodecode<\/p>\n<p>);<\/p>\n<p>&#8220;`<\/p>\n<p>This avoids hardcoding a base64 string into your CSS and keeps the image as a separate asset.<\/p>\n<h3>5. Register the Plugin<\/h3>\n<p>In your Astro config file (<code>astro.config.mjs<\/code>codecodecodecode), add the plugin:<\/p>\n<p>&#8220;`javascript<\/p>\n<p>import { defineConfig } from &#8216;astro\/config&#8217;;<\/p>\n<p>import emdash from &#8216;@astrojs\/mdash&#8217;;<\/p>\n<p>import adminBrandPlugin from &#8216;.\/plugins\/admin-brand\/hooks&#8217;;<\/p>\n<p>export default defineConfig({<\/p>\n<p>  integrations: [<\/p>\n<p>    emdash({<\/p>\n<p>      plugins: [adminBrandPlugin],<\/p>\n<p>    }),<\/p>\n<p>  ],<\/p>\n<p>});<\/p>\n<p>&#8220;`<\/p>\n<p>Now rebuild your site with <code>npm run build<\/code>codecodecodecode and redeploy. The admin dashboard will reflect your brand.<\/p>\n<h2>Comparison Table: CSS-Only vs. Plugin Approach<\/h2>\n<table class=\"mw-table\">\n<thead>\n<tr>\n<th>Aspect<\/th>\n<th>CSS-only override<\/th>\n<th>Brand plugin (this tutorial)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Update resilience<\/td>\n<td>Broken after any EmDash core CSS change<\/td>\n<td>Styles are isolated; update selectors in one file<\/td>\n<\/tr>\n<tr>\n<td>Multi-instance management<\/td>\n<td>Fork styles per site<\/td>\n<td>Single plugin with configurable variables<\/td>\n<\/tr>\n<tr>\n<td>Plugin conflict risk<\/td>\n<td>High \u2013 any plugin can stomp on selectors<\/td>\n<td>Zero \u2013 plugin runs in its own sandbox<\/td>\n<\/tr>\n<tr>\n<td>Deployment overhead<\/td>\n<td>Manual CSS copy each time<\/td>\n<td>Code in version control, deploy with <code>npm run deploy<\/code>codecodecodecode<\/td>\n<\/tr>\n<tr>\n<td>Requires Cloudflare paid plan?<\/td>\n<td>No<\/td>\n<td>No for basic CSS injection; yes for advanced logic requiring dynamic workers<\/td>\n<\/tr>\n<tr>\n<td>Accessibility scalability<\/td>\n<td>Hard to maintain consistent contrast<\/td>\n<td>Variables centralised; WCAG compliance easier<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>When the Plugin Approach Still Falls Short<\/h2>\n<p>This method assumes your brand customisation stays within what CSS can express. What if you need to rearrange the admin sidebar, add a custom widget, or change the user role permissions UI? Those require deeper hooks. EmDash\u2019s plugin API currently offers a limited set of hooks (<code>admin:afterRender<\/code>codecodecodecode, <code>content:beforeSave<\/code>codecodecodecode, etc.). If you need to modify JavaScript-driven components (like the block editor toolbar), you\u2019re back to patching source files\u2014or waiting for Cloudflare to expose more hooks.<\/p>\n<p>Also, note that the <code>admin:afterRender<\/code>codecodecodecode hook runs on every admin page load. <a href=\"https:\/\/overcentral.com\/en\/home-insurance-disaster-coverage-82139\/\" title=\"Check If Your Home Insurance Covers Disaster Damage\" data-iacss-internal=\"1\">If your<\/a> CSS files are large, you\u2019ll add a few kilobytes to each HTML response. For most sites, that\u2019s negligible. But if you\u2019re targeting mobile users with slow connections, consider inlining only the critical override and linking to an external stylesheet.<\/p>\n<h2>The 80\/20 Rule of Admin Branding<\/h2>\n<p>For 80% of users (single-author blog, small business), the CSS snippet approach is fine. But if you\u2019re in the 20%\u2014developers managing multiple clients, agencies needing airtight deployments, or brands with strict visual identity\u2014invest the extra hour to build a proper plugin. It\u2019s the difference between a customization that lasts three months and one that survives three major EmDash updates.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The standard advice you\u2019ll hear everywhere? \u201cJust drop a custom CSS snippet into the admin theme.\u201d It\u2019s clean, it\u2019s simple, and it works\u2014until it doesn\u2019t. Most tutorials stop there. But if you\u2019re running a multi-author site, an agency with ten client instances, or a brand with strict accessibility requirements, that one-liner leaves you exposed. CSS [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":99282,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"fifu_image_url":"https:\/\/cards.overcentral.com\/cards\/en\/98023.png","fifu_image_alt":"How to Customize the EmDash Admin Dashboard for Your Brand","footnotes":""},"categories":[31],"tags":[],"class_list":["post-98023","post","type-post","status-publish","format-standard","has-post-thumbnail","category-technology"],"fifu_image_url":"https:\/\/cards.overcentral.com\/cards\/en\/98023.png","fifu_image_alt":"How to Customize the EmDash Admin Dashboard for Your Brand","_links":{"self":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98023","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/comments?post=98023"}],"version-history":[{"count":1,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98023\/revisions"}],"predecessor-version":[{"id":99283,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/posts\/98023\/revisions\/99283"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/media\/99282"}],"wp:attachment":[{"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/media?parent=98023"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/categories?post=98023"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/overcentral.com\/en\/wp-json\/wp\/v2\/tags?post=98023"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}