Separate JS & CSS for admin panel in Phoenix

SeriesPhoenix on Rails clock3 min read

It's a common need to have separate precompiled assets for admin panel or other heavy pages. Here's how to do it.

Note: In this guide I'm using Brunch as it's a default asset manager in Phoenix. Of course, as explained in Phoenix guide, it's easy to use different asset build tool like Webpack, but that's out of the scope of this article.

Why bother

Well, one of the main objectives for web developer should be to keep client-consumed website as lightweight as possible. Even if we include appropriate clauses in JavaScripts and stylesheets to prohibit them from applying to non-admin pages, browsers will still have to: 

  • download them, slowing down the initial entry and each one after you've released new assets, which by the way may include zero changes relevant to non-admin pages
  • process them, which is even worse as it'll slow down rendering of every page (and make no mistake - even with modern JS engines, it will noticeably affect your page loads)
  • handle errors that happen in admin JS, possibly canceling execution of other key scripts

Admin panels usually come with some pretty heavy assets like WYSIWYG editors, image uploaders or UI plugins for drag-n-drop and alike. I'd definitely recommend keeping those away from the main website unless it shares those functionalities with the admin panel.

Directory structure

There's no single convention for namespacing assets for multiple scopes, but I've invented one that plays nice with existing conventions and makes things easy for Brunch. You'll end up with the same directories as the default Phoenix asset layout, that is:

  • js for ES6-enabled, module-wrapped JavaScripts
  • css for stylesheets
  • vendor for JavaScripts that don't get processed at all

So, go ahead and create the following directory structure:


You should move existing app-related files into app and admin-related ones to admin. If some of new directories end up empty, you can create empty .keep files in them in order to force them to stay in version control and be available when needed.

You may have noticed that I didn't include the assets directory in the party. That's because the contents from hypothetical admin/assets and app/assets would be copied into the same target directory by Brunch, possibly colliding with each other if you'd ever put the same file in both. Therefore, web/static/assets should stay as is and it's up to you to decide on organization within it (perhaps just by adding web/static/assets/admin).

Configuring Brunch

Now, replace the files key in brunch-config.js with the following:

files: {
  javascripts: {
    joinTo: {
      'js/app.js': /^(web\/static\/app)/,
      'js/admin.js': /^(web\/static\/admin)/

  stylesheets: {
    joinTo: {
      'css/app.css': /^(web\/static\/app)/,
      'css/admin.css': /^(web\/static\/admin)/

  templates: {
    joinTo: 'js/app.js'

We want to have the same behavior of the vendor directory as before, so no module-wrapping and no Babel for ES6. While module-wrapping will still be disabled to every vendor directory including our new ones, we need to disable Babel manually with the following:

plugins: {
  babel: {
    /* ... */
    ignore: [

That's it. You can run brunch build to see if it works. Now you can use your new assets in the layout with paths like those:

static_path @conn, "/css/app.js"
static_path @conn, "/css/app.css"
static_path @conn, "/css/admin.js"
static_path @conn, "/css/admin.css"


You can apply the same technique to create virtually unlimited number of precompiled assets. Don't hesitate to comment. If anything goes wrong I'll gladly help. 

That said, I'm leaving you in peace with your per-scope Brunch.