docs/subsystems/html-css.md
There are two high-level sections of CSS: the "portico" (logged-out
pages like /help/, /login/, etc.), and the app. The Zulip
application's CSS can be found in the web/styles/ directory, while
the portico CSS lives under the web/styles/portico/ subdirectory.
To generate its CSS files, Zulip uses PostCSS and a number of PostCSS plugins, including postcss-nesting, whose rules are derived from the CSS Nesting specification.
If you aren't experienced with doing web development and want to make CSS changes, we recommend reading the excellent Chrome developer tools guide to the Elements panel and CSS, as well as the section on viewing and editing CSS to learn about all the great tools that you can use to modify and test changes to CSS interactively in-browser (without even having the reload the page!).
Our CSS is formatted with Prettier. You can
ask Prettier to reformat all code via our linter
tool with tools/lint --only=prettier --fix.
You can also integrate it with your
editor.
Zulip's development environment has hot code-reloading configured, so changes made in source files will immediately take effect in open browser windows, either by live-updating the CSS or reloading the browser window (following backend changes).
Without care, it's easy for a web application to end up with thousands of lines of duplicated CSS code, which can make it very difficult to understand the current styling or modify it. We would very much like to avoid such a fate. So please make an effort to reuse existing styling, clean up now-unused CSS, etc., to keep things maintainable.
Opt to write CSS in CSS files. Avoid using the style= attribute in
HTML except for styles that are set dynamically. For example, we set
the colors for specific channels ({{stream_color}}) on different
elements dynamically, in files like user_stream_list_item.hbs:
<span
class="stream-privacy-original-color-{{stream_id}} stream-privacy filter-icon"
style="color: {{stream_color}}">
But for most other cases, its preferable to define logical classes and
put your styles in external CSS files such as zulip.css or a more
specific CSS file, if one exists. See the contents of the web/styles/
directory.
Ideally, do this by reusing existing CSS declarations, so that any improvements we make to the styling can improve all similar UI elements.
This makes it much easier to read the code and use git grep to find
where a particular class is used.
Don't use the tag name in a selector unless you have to. In other words,
use .foo instead of span.foo. We shouldn't have to care if the tag
type changes in the future.
Additionally, multi-word class and ID values should be hyphenated,
also known as kebab case. In HTML, opt for class="my-multiword-class",
with its corresponding CSS selector as .my-multiword-class.
When changing any part of the Zulip CSS, it's important to check that the new CSS looks good at a wide range of screen widths, from very wide screen (e.g., 1920px) all the way down to narrow phone screens (e.g., 480px).
For complex changes, it's definitely worth testing in a few different browsers to make sure things look the same.
Templates are automatically recompiled in development when the file is saved; a refresh of the page should be enough to display the latest version. You might need to do a hard refresh, as some browsers cache webpages.
Variables can be used in templates. The variables available to the template are called the context. Passing the context to the HTML template sets the values of those variables to the value they were given in the context. The sections below contain specifics on how the context is defined and where it can be found.
For text generated in the backend, including logged-out ("portico")
pages and the web app's base content, we use the Jinja2 template
engine (files in templates/zerver).
The syntax for using conditionals and other common structures can be found here.
The context for Jinja2 templates is assembled from a couple places:
zulip_default_context in zerver/context_processors.py. This is
the default context available to all Jinja2 templates.
As an argument in the render call in the relevant function that
renders the template. For example, if you want to find the context
passed to index.html, you can do:
$ git grep zerver/app/index.html '*.py'
zerver/views/home.py: response = render(request, 'zerver/app/index.html',
The next line in the code being the context definition.
For text generated in the frontend, live-rendering HTML from
JavaScript for things like the main message feed, we use the
Handlebars template engine (files in web/templates/) and
sometimes work directly from JavaScript code (though as a policy
matter, we try to avoid generating HTML directly in JavaScript
wherever possible).
The syntax for using conditionals and other common structures can be found here.
There's no equivalent of zulip_default_context for the Handlebars
templates.
Handlebars is in our package.json and thus ends up in node_modules; We use
handlebars-loader to load and compile templates during the webpack bundling
stage. In the development environment, webpack will trigger a browser reload
whenever a template is changed.
All user-facing strings (excluding pages only visible to sysadmins or developers) should be tagged for translation.
Zulip uses TippyJS for its tooltips.
This section documents additional information that may be useful when developing new features for Zulip that require front-end changes, especially those that involve adding new files. For a more general overview, see the new feature tutorial.
Our dependencies documentation has useful relevant background as well.
Zulip's frontend is primarily JavaScript in the web/src directory;
we are working on migrating these to TypeScript modules. Stylesheets
are written in CSS extended by various PostCSS plugins; they are
converted from plain CSS, and we have yet to take full advantage of
the features PostCSS offers. We use Webpack to transpile and build JS
and CSS bundles that the browser can understand, one for each entry
points specified in web/webpack.*assets.json; source maps are
generated in the process for better debugging experience.
In development mode, bundles are built and served on the fly using
webpack-dev-server with live reloading. In production mode (and when creating a
release tarball using tools/build-release-tarball), the
tools/update-prod-static tool (called by both tools/build-release-tarball
and tools/upgrade-zulip-from-git) is responsible for orchestrating the
webpack build, JS minification and a host of other steps for getting the assets
ready for deployment.
You can trace which source files are included in which HTML templates
by comparing the entrypoint variables in the HTML templates under
templates/ with the bundles declared in web/webpack.*assets.json.
To add a static file to the app (JavaScript, TypeScript, CSS, images, etc),
first add it to the appropriate place under static/.
package.json for management by pnpm, this allows them to be upgraded easily
and not bloat our codebase. Run ./tools/provision for pnpm to install the
new packages and update its lock file. You should also update
PROVISION_VERSION in version.py in the same commit.web/third/. Tag the commit with "[third]" when adding or
modifying a third-party package. Our goal is to the extent possible
to eliminate patched third-party code from the project.web/src. Ideally,
new modules should be written in TypeScript (details on this policy below).web/styles.web/src/portico.web/icons are compiled into
custom icon webfonts by webfont-loader according to the
web/icons/template.hbs template. See
icons for more details on adding icons.For your asset to be included in a development/production bundle, it
needs to be accessible from one of the entry points defined either in
web/webpack.assets.json or web/webpack.dev-assets.json.
app bundle by importing it
in web/src/bundles/app.ts.web/src/bundles/common.ts which itself is imported to the
app and common bundles./devlogin) create a new entry
point in web/webpack.dev-assets.json or it's used in both
production and development (e.g., /stats) create a new entry point
in web/webpack.assets.json. Use the bundle macro (defined in
templates/zerver/base.html) in the relevant Jinja2 template to
inject the compiled JS and CSS.If you want to test minified files in development, look for the
DEBUG = line in zproject/default_settings.py and set it to False.
A few useful notes are:
/home/zulip/prod-static. When a new version is deployed, before the
server is restarted, files are copied into that directory.favicon.ico) gets a new name
(e.g., favicon.c55d45ae8c58.ico) that contains a hash in it. Each
deployment, has a manifest file
(e.g., /home/zulip/deployments/current/staticfiles.json) that maps
codebase filenames to serving filenames for that deployment. The
benefit of this VFL approach is that all the static files for past
deployments can coexist, which in turn eliminates most classes of
race condition bugs where browser windows opened just before a
deployment can't find their static assets. It also is necessary for
any incremental rollout strategy where different clients get
different versions of the site.rendered_content of past messages, and thus cannot be removed
without breaking the rendering of old messages (or doing a
mass-rerender of old messages).JavaScript modules in the frontend are ES6
modules
that are transpiled by
webpack.
Any variable, function, etc. can be made public by adding the
export
keyword,
and consumed from another module using the import
statement.
New modules should ideally be written in TypeScript (though in cases where one is moving code from an existing JavaScript module, the new commit should just move the code, not translate it to TypeScript). TypeScript provides more accurate information to development tools, allowing for better refactoring, auto-completion and static analysis. TypeScript also uses the ES6 module system. See our documentation on TypeScript static types.
Webpack does not ordinarily allow modules to be accessed directly from
the browser console, but for debugging convenience, we have a custom
webpack plugin (web/debug-require-webpack-plugin.ts) that exposes
a version of the require() function to the development environment
browser console for this purpose. For example, you can access our
people module by evaluating
let people = require("./src/people"), or the third-party lodash
module with let _ = require("lodash"). This mechanism is not a
stable API and should not be used for any purpose other than
interactive debugging.
We have one module, zulip_test, that’s exposed as a global variable
using expose-loader for direct use in Puppeteer tests and in the
production browser console. If you need to access a variable or
function in those scenarios, add it to zulip_test. This is also
not a stable API.