Hi everyone! I’m pleased to share with you a feature we’ve been working on for Dash 2.x: "Dash /pages
". This is one of many new features coming soon in 2.x
Dash /pages
started from a discussion with community member @AnnMarieW about how we could simplify creating multi-page Dash applications.
Background
Currently, creating a proper multi-page Dash app involves a fair amount of boilerplate code:
- A
dcc.Location
callback that listens to the URLpathname
and returns different layouts - Importing files from a folder and passing that content into that callback
- Defining a bunch of
dcc.Link
components that are careful to use the samehref
paths as defined in thedcc.Location
callback
and a heckuva lot more code if you want an A-Grade multi-page app experience:
- A
clientside_callback
to update the tab’s title on page navigation - Setting
suppress_callback_exceptions=True
orvalidate_layout
to avoid exceptions from callbacks defined in the pages - Defining the URL navigation components within
app.layout
including duplicating the the URL paths used in thedcc.Location
callback - Setting
<title>
and<meta type="description">
tags in the htmlindex_string
if you want your page to be indexed by search engines or display nicely in Slack or social media - A handful of
flask.redirect
functions when you change URLs - A “404 Not Found” page if someone enters an invalid URL
Introducing Dash /pages
After a few prototypes, @AnnMarieW and I came up with a new API that provides a new simple and standard way to create multi-page Dash applications. With this new API, creating a multi-page app involves 4 steps:
1./pages
- Add your new pages to a folder called pages/
. Each file should contain a function or variable called layout
2. dash.register_page
- Within each file within pages/
, simply call dash.register_page(__name__)
.
3. dash.page_container
- Pass dash.page_container
into app.layout
4. dash.page_registry
- Create headers and navbars by looping through dash.page_registry
: an OrderedDict
containing the page’s paths, names, titles, and more.
That’s it! Dash will now display the page when you visit that page in your browser and it will automatically hook in all of the functionality listed above (dcc.Location
, redirects, 404s, updating the page title and description, setting validate_layout
, and more).
Quickstart
pages/home.py
dash.register_page(__name__, path='/')
def layout():
# ...
return html.Div([...])
pages/historical_analysis.py
dash.register_page(__name__)
def layout():
# ...
return html.Div([...])
app.py
:
import dash
import dash_bootstrap_components as dbc
app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
app.layout = dbc.Container([
dbc.NavbarSimple([
dbc.NavItem(dbc.NavLink(page['name'], href=page['path'])),
for page in dash.page_registry
]),
dash.page_container
])
Then you could visit:
-
http://localhost:8050/ in your browser and the
layout
fromhome
would be displayed. -
http://localhost:8050/historical-analysis in your browser and the
layout
fromhistorical_analysis
would be displayed.
The URL path is created from the filename __name__
(replacing _
with -
) and be manually overridden in dash.register_page(path=...)
. For example:
pages/historical_analysis.py
dash.register_page(__name__, path='/historical')
def layout():
# ...
return html.Div([...])
In this case, the layout would be displayed at http://localhost:8050/historical instead of http://localhost:8050/historical-analysis.
Creating Navigation
The pages that are registered can be accessed with dash.page_registry
. This can be used to create navigation headers in your app.py
.
For example, with dash-bootstrap-components
:
app.py
import dash
import dash_bootstrap_components as dbc
app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
app.layout = dbc.Container([
dbc.NavbarSimple([
dbc.NavItem(dbc.NavLink(page['name'], href=page['path'])),
for page in dash.page_registry
]),
dash.page_container
])
Or for our Dash Enterprise customers using the Design Kit
import dash_design_kit as ddk
app = dash.Dash(__name__)
app.layout = ddk.App([
ddk.Header([
ddk.Menu(dcc.Link(page['name'], href=page['path']))
for page in dash.page_registry
]),
dash.page_container
])
dash.page_container
dash.page_container
defines where the page’s content should render.
Often this will be below a navigation header or to the right of a navigation sidebar.
Advanced Features
These features are all optional. If you don’t supply values here, the framework will take a best guess and supply them for you.
Custom Meta Tags & Titles
The page’s title defines what you see in your browser tab and what would appear as the website’s title in search results. By default, it is derived from the filename but it can also be set with title=
dash.register_page(__name__, title='Custom page title')
Similarly, the meta description can be set with description=
and the meta image can be set with image=
. Both of these tags are used as the preview when sharing a URL in a social media platforms. They’re also used in search engine results.
By default, Dash will look through your assets/
folder for an image that matches the page’s filename or else an image called app.<image_extension>
(e.g. app.png
or app.jpeg
) or logo.<image_extension>
(all image extensions are supported).
This image URL can also be set directly with app.register_page(image=...)
e.g.
app.register_page(__name__, image='/assets/page-preview.png')
dash.page_registry
dash.page_registry
is an OrderedDict
. The keys are the module as set by __name__
, e.g. pages.historical_analysis
. The value is a dict with the parameters passed into register_page
: path
name
, title
, description
, image
, order
, and layout
. If these parameters aren’t supplied, then they are derived from the filename.
For example:
pages/historical_analysis.py
dash.register_page(__name__)
print(dash.page_registry)
would print:
OrderedDict([
('pages.historical_analysis', {
'module': 'pages.historical_analysis',
'name': 'Historical analysis',
'title': 'Historical analysis',
'description': 'Historical analysis',
'order': None,
}
])
Whereas:
pages/outlook.py
dash.register_page(__name__, path='/future', name='Future outlook', order=4)
print(dash.page_registry)
would print:
OrderedDict([
('pages.outlook', {
'module': 'pages.outlook',
'path': '/future',
'name': 'Future outlook',
'title': 'Future outlook',
'description': 'Future outlook',
'order': 4,
}
])
OrderedDict values can be accessed just like regular dictionaries:
>>> print(dash.page_registry['pages.historical_analysis']['name'])
'Historical analysis'
The order of the items in page_registry
is based off of the optional order=
parameter:
dash.register_page(__name__, order=10)
If it’s not supplied, then the order is alphanumeric based off of the filename. This order is important when rendering the page menu’s dynamically in a loop. The page with the path /
has order=0
by default.
Redirects
Redirects can be set with the redirect_from=[...]
parameter in register_page
:
pages/historical_analysis.py
dash.register_page(
__name__,
path='/historical',
redirect_from=['/historical-analysis', '/historical-outlook']
)
Custom 404 Pages
404 pages can display content when the URL isn’t found. By default, a simple content is displayed:
html.Div([
html.H2('404 - Page not found'),
html.Div(html.A('Return home', href='/')
])
However this can be customized by creating a file called not_found_404.py
Defining Multiple Pages within a Single File
You can also pass layout=
directly into register_page
. Here’s a quick multi-page app written in a single file:
app.register_page('historical_analysis', path='/historical-analysis', layout=html.Div(['Historical Analysis Page'])
app.register_page('forecast', path='/forecast', layout=html.Div(['Forecast Page'])
app.layout = dbc.Container([
dbc.NavbarSimple([
dbc.NavItem(dbc.NavLink(page['name'], href=page['path'])),
for page in dash.page_registry
]),
dash.page_container
])
However, we recommend splitting out the page layouts into their own files in pages/
to keep things more organized and to keep your files from becoming too long!
Available to Preview
A prototype has been built and open-sourced in GitHub - plotly/dash-multi-page-app-plugin. Follow the instructions in that repository to give it a try! In this preview version it is implemented as a Dash plugin, so you need to set:
dash.Dash(__name__, plugins=[pages_plugin])
The source code for this functionality is available in pages_plugin.py. It uses standard Dash features like dcc.Location
, plugins, and interpolate_index
, so if you’ve created a multi-page Dash app before it should look familiar.
In Dash 2.x, this will be integrated into the library and you won’t need to set the plugin
.
We would love to hear your thoughts, feedback, and suggestions on this new feature. Let us know by leaving a reply below