slac is the Drupal front-end theme for SLAC National Accelerator Laboratory
websites. It is derived from Gesso, a
Sass-based starter theme that outputs accessible HTML5
markup. It uses a mobile-first responsive approach and leverages
SMACSS to organize styles. This encourages a
component-based approach to theming through the creation of discrete, reusable
UI elements. The theme is heavily integrated with
Storybook and the Component
Libraries module, allowing Drupal
and Storybook to share the same markup.
The theme is distributed as a Composer package
(slac/slac-drupal-profile-theme, type drupal-theme) rather than being copied
into a site's web/themes/custom/ directory. See
Installation below.
This theme currently tracks Gesso 5.4.6. Component markup, design tokens and styles have diverged substantially from upstream and are intentionally not kept in sync; see Relationship to upstream Gesso.
To submit bug reports or feature requests for this theme, use this repository's issue queue. For upstream Gesso itself, see the Gesso Drupal project page, the Gesso GitHub repo, and the Gesso Storybook demo site.
The following packages need to be installed on your system in order to compile and use this theme.
-
Node version 22 (LTS), as pinned in
.nvmrc. CI (ci.yml,build-assets.yml,publish-demo-site.yml) reads the same file. -
npm version 10.7.0 or greater.
This theme is published as a Composer package through the SLAC Satis
repository. It is not meant to be copied into web/themes/custom/; releases
are built by CI and consumed by Composer.
-
Make sure the SLAC Satis repository is configured in your site's
composer.json, then require the theme:composer require slac/slac-drupal-profile-theme
Because
composer.jsondeclares"type": "drupal-theme", your site's Composer installer paths will place it in the appropriate themes directory asslac. -
Enable the theme, then set it as the default theme on the Appearance admin page:
drush theme:enable slac
-
Enable the SLAC Helper (
slac_helper) module. Unlike upstream Gesso, this module is not bundled in this repository — it is a separate package that must be required and installed on its own. It is listed inslac.info.ymldependencies, so it must be present for the theme to function.slac_helperprovides the theme's PHP-side Twig filters, includingunique_id. -
Install the Component Libraries module. Since many of the Drupal templates reference twig files inside Storybook using Twig namespaces, this module is required for the theme to function. It is listed in
slac.info.ymldependencies. -
Install the Twig Tweak module. It is also listed in
slac.info.ymldependencies. -
Optional: Install the Twig Field Value module. This is not required, but it can make working with Twig templates easier. Please note, however, that using the
|field_valueTwig filter from this module will break Drupal’s QuickEdit functionality. -
Optional: Install the Background Images Formatter module and its Responsive Background Images Formatter submodule. This is not required, but it will allow you to use images uploaded to Drupal as background images, with different image sizes at different breakpoints.
The screenshot shown on the Appearance admin page is screenshot.png in the
theme root.
For development, you can set the theme up as part of a Drupal site or work only in Storybook. The theme includes npm tasks to compile design tokens, CSS, JS, Storybook, and the SVG sprite using webpack.
To use these tasks, first run the following npm command in the theme folder to install node dependencies.
npm ciTo compile the theme, start Storybook, and watch for changes run the following command in the theme directory:
npm run devOpen localhost:6006 to view Storybook. If you’re using Docker (or some other container engine) for local development, this might be mapped to a custom domain or a port on a custom domain such as storybook.ddev.site or site.ddev.site:6006.
If you add new SCSS and/or JS files, you will need to restart webpack by
canceling and then re-running npm run dev. New files will not be processed
until webpack restarts. Errors will also be shown for duplicate filenames.
The full set of scripts defined in package.json:
| Script | What it does |
|---|---|
npm run start |
One-off build of the design tokens only (webpack.theme-config.js in development mode). It does not compile CSS or JS. It is a prerequisite for the watchers, which is why watch and dev run it first. |
npm run watch-theme |
Watches and rebuilds the theme's CSS, JS and SVG sprite (webpack.dev.js). |
npm run watch-design-tokens |
Watches and rebuilds the generated design-token Sass partial and JS object (webpack.theme-config.js). |
npm run watch |
start, then watch-theme and watch-design-tokens concurrently. No Storybook. Use this when you are working against a real Drupal site. |
npm run dev |
start, then watch-theme, watch-design-tokens and storybook concurrently. Use this when you are working in Storybook. |
npm run storybook |
Storybook dev server on port 6006, without any theme watchers. |
npm run build |
Full production build: design tokens (webpack.theme-config.js) followed by CSS/JS/sprite (webpack.production.js). This is the build CI runs for releases. |
npm run build-storybook |
build, then a static Storybook export into storybook/. |
npm run eslint |
Lints source/** JavaScript, excluding story files. |
npm run stylelint |
Lints source/**/*.scss. |
npm test |
Runs eslint then stylelint. Does not build anything. |
npm run component |
Scaffolds a new component (see below). |
Note that the webpack builds run ESLint and Stylelint as plugins, so lint
failures break the build. The standalone eslint and stylelint scripts are
there for when you want to check linting without waiting for a full build.
Run npm run component to create boilerplate files for a new component. This is
the recommended approach as it will set up basic Twig and Storybook files that
you can modify.
Running the command without arguments will prompt you for the component details:
npm run componentYou can also pass arguments to skip the prompts:
npm run component -- --name my-component --folder 03-components| Option | Description |
|---|---|
--name <name> |
Component name (required) |
--folder <folder> |
Component location, e.g., 03-components (required) |
--title <title> |
Human-readable title (defaults to Capital Case of name) |
--subfolder <name> |
Optional subfolder within the component location |
--no-modular-sass |
Add styles to the global stylesheet instead of a separate file |
--js |
Include a JavaScript file |
--help, -h |
Show help message |
Name your stories files [component].stories.jsx. See
source/03-components/menu/menu.stories.jsx for an example. .storybook/main.js
picks up source/**/*.stories.@(js|jsx|ts|tsx).
Prose documentation pages are plain [name].mdx files, not
[name].stories.mdx. Storybook 8 removed support for MDX files that define
stories, so an MDX file may only contain documentation; the stories it documents
have to live in a sibling .stories.jsx. Existing examples:
source/01-global/global.mdx,
source/03-components/dropdown-menu/dropdown-menu.mdx.
Stories use Component Story Format 2: the default export is the component meta,
and each story is a function of its args, with args (and, if needed,
storyName) attached as properties. The stories are exported as a list.
import parse from 'html-react-parser';
import twigTemplate from './menu.twig';
import data from './menu.yml';
const settings = {
title: 'Components/Menu/Default',
};
const Default = args =>
parse(
twigTemplate({
...args,
})
);
Default.args = { ...data };
export default settings;
export { Default };Story names come from the export name (LargeCard is shown as "Large Card"),
or from an explicit LargeCard.storyName = '...'. Storybook's own indexer
does not do that for export lists, so .storybook/main.js wraps it.
npm run component scaffolds upstream Gesso's CSF3 form (a plain object with a
render function). It renders too, but convert it to CSF2 to match the rest of
the theme.
Storybook 7 and later load only the current story's imports, not every story file. So:
- A story imports its own component's stylesheet and script (for example
import './menu.scss';andimport './menu.es6';). - A story that renders another component (through a Twig
include,embedorextends) also imports that component's stories file (for exampleimport '../tooltip/tooltip.stories';), or its stylesheet where it has no stories file. That mirrors theattach_library()calls Drupal would follow. .storybook/preview.jsloadsdist/css/styles.cssand the site-wide behaviours of theslac/globallibrary (arrow-link,external-link,transitions) for every story.
Shared story helpers live in source/06-utility/storybookHelper.jsx.
.storybook/decorators.jsx (upstream's) exports a withGlobalWrapper
decorator that no story currently uses; the global decorator that runs
Drupal.attachBehaviors() after each render is defined in
.storybook/preview.js.
To match Storybook to your site’s branding, change the colors, brand title,
brand logo and base font in .storybook/theme.js, which
.storybook/manager.js imports and passes to addons.setConfig(). Web fonts
are loaded in .storybook/manager-head.html (for the Storybook UI itself) and
.storybook/preview-head.html (for the rendered stories). See the Storybook
docs for more
information about and examples of theming.
Storybook 10 validates the Host header on dev-server requests. When you access
Storybook through the DDEV router (https://<project-name>.ddev.site:6006), the
hostname must be allowlisted in .storybook/main.js.
By default, this theme reads DDEV_HOSTNAME or VIRTUAL_HOST from the environment
(DDEV sets VIRTUAL_HOST in the Storybook container) and uses that hostname. If
neither variable is set, any *.ddev.site hostname is allowed instead. Access
via localhost:6006 does not require additional configuration.
If you use a different reverse proxy or custom local domain, add its hostname to
core.allowedHosts in .storybook/main.js. See the Storybook core
docs for
details.
Sass can be compiled as part of the global styles.css file or to individual CSS files for use in a Drupal library.
@use is used to import Sass variables, mixins, and/or functions into
individual SCSS files. @import is discouraged by the Sass team and will
eventually be phased out..
This means that most files will start with @use '00-config' as *;. This allows
you to use the design token accessor functions without an additional namespace.
Other functions and mixins can be used similarly. Note that to avoid namespace
collisions, only this theme's own variables, mixins, and functions (those
forwarded from source/00-config) should be used with *.
All Sass files that are compiled to individual CSS files must have a unique filename, even if they are in different directories.
Prefix the name of your Sass file with _, e.g. _card.scss. Add it to the
appropriate aggregate file (i.e. _components.scss).
DO NOT prefix the name of your Sass file with _, e.g. menu.scss. Import the
config and global aggregate files. Import your SCSS file at the top of your
Storybook file. See dropdown-menu.stories.jsx for an example. Don’t forget to
add it to the slac.libraries.yml file as well.
Stylelint and Prettier are used to lint CSS and SCSS files. Warnings will break the build, so if you have a valid reason to break Stylelint rules you can have it ignore code in two ways:
-
Add
// stylelint-disable-next-lineto the line just before where the Stylelint warning is triggered. -
To ignore several lines, add
// stylelint-disablebefore the code in question and add// stylelint-enableafterwards.
In both cases above, please add a comment about the valid reason to disable the Stylelint rule(s) in your use case.
The Stylelint rules can be changed in the .stylelintrc.yml file. By default,
the theme extends
stylelint-config-sass-guidelines
and enables the
stylelint-prettier plugin
(which reports Prettier formatting differences as Stylelint errors), plus
stylelint-order and a local plugin/selector-pseudo-class-lvhfa rule from
lib/stylelintLVHFA.js, with some additional customizations.
The Prettier config can be changed in the .prettierrc file.
You can also run Stylelint on its own with npm run stylelint.
JavaScript can be compiled to individual JS files for use in a JavaScript
library or included within a different JS file. JS files that use modern
(ES2015+) syntax must be named [name].es6.js, but this is not required by the
compiler. JavaScript files should go in the appropriate folder under source
(e.g., source/03-components/menu for menu-related JavaScript). There is not a
separate folder for JS files as there was in older versions of this theme.
All JavaScript files must have a unique filename, even if they are in different directories.
Prefix the name of your JavaScript file with _, e.g. _Menu.es6.js. Import it
to the appropriate JavaScript file(s), (i.e. primary-menu.es6.js).
DO NOT prefix the name of your JS file with _. Import your JS file at the top
of your Storybook file. See dropdown-menu.stories.jsx for an example. Don’t
forget to add it to the slac.libraries.yml file as well.
Any library you create in slac.libraries.yml that includes an individual
component script must include slac/common as a dependency. (In most cases, you
will also add core/drupal as a dependency, if you are using the Drupal
object anywhere in your code.) common.js is generated by both the production
build and the development watcher (webpack.common.js has held the
splitChunks configuration since Gesso 5.4.6), and contains JavaScript that is shared across two or more components, so that it is
not bundled multiple times on the page. The recommended practice is for each
library to declare its dependencies, even if some of them are repeated across
multiple libraries and/or shared with global. This ensures that Drupal will
always load the dependencies before loading any library that depends on them.
See the dropdown_menu library in slac.libraries.yml as an example.
The common JS file is created using the Webpack SplitChunksPlugin.
To change how it behaves, update optimization.splitChunks in webpack.common.js. You may also need to
update slac_library_info_build in includes/libraries.inc to change what
files are included in the slac/common library. We recommend using the default
setup unless you have a specific use case that requires advanced configuration.
ESLint and Prettier are used to lint JavaScript files. If you have a valid reason to break one of the rules, you can ignore a specific line using any of the options in the ESLint documentation.
Please add a comment about the valid reason to disable the ESLint rule(s) in your use case.
The ESLint config can be changed in the eslint.config.js file. The theme
follows the Forum One JavaScript standards,
which mostly follow the ESLint recommended config. For React files, there are
additional JSX-specific linting rules.
A relaxed variant used by the dev webpack build lives in eslint.dev.config.js.
The Prettier config can be changed in the .prettierrc file.
You can also run ESLint on its own with npm run eslint.
Upstream Gesso no longer ships jQuery. This theme deliberately keeps it, because
two components require it: source/03-components/dropbutton/dropbutton.es6.js
(a port of Drupal core's jQuery-based dropbutton) and
source/03-components/addtocal/addtocal-a11y.es6.js (the addtocal contrib
module's JS requires jQuery).
jQuery is therefore retained in the following places, all of which must stay in sync:
-
jqueryinpackage.jsondependencies. -
jquery: 'jQuery'in theexternalsblock ofwebpack.common.js, so it is treated as a Drupal-provided global rather than bundled. -
core/jqueryin thedependenciesof both thedropbuttonandaddtocal_a11ylibraries inslac.libraries.yml.
Storybook has no jQuery external: there is no Drupal-provided jQuery on a
Storybook page, so import jQuery from 'jquery' bundles the real package into
the stories that need it.
Import it at the top of a file the same way Drupal and once are imported:
import jQuery from 'jquery';If a future refactor removes the last jQuery consumer, remove all three entries above together.
TypeScript is supported for component scripts. The webpack entry glob picks up
source/**/!(*.stories).{cjs,js,ts}, so a component script may be named
[name].es6.ts instead of [name].es6.js and will compile to the same
dist/js/[name].es6.js output. The same "no leading underscore, unique
filename" rules apply. .ts/.tsx files are handled by
ts-loader in transpileOnly mode,
with type checking done out of band by
fork-ts-checker-webpack-plugin,
so type errors are reported without slowing the bundle down.
Because resolve.extensionAlias maps .es6 to ['.es6.ts', '.es6.js'], an
existing import Foo from './_Foo.es6' keeps working when _Foo.es6.js is
renamed to _Foo.es6.ts. This means files can be migrated one at a time.
Compiler options live in tsconfig.json. The compilerOptions.paths block
resolves the module specifiers that are webpack externals at build time, so
that the type checker and editors can still find them:
drupal,drupalSettingsandonceresolve to the Storybook stubs in.storybook/stubs/.jqueryresolves to the real package (node_modules/jquery/dist/jquery.js).
lib/ is excluded from the project's type checking and has its own
lib/tsconfig.json.
The theme uses the configuration file
source/00-config/config.design-tokens.yml to manage its design tokens. The npm
build and dev tasks will automatically generate a global Sass map to easily pull
design tokens into individual SCSS files.
The following Sass functions can be used to access the tokens defined in
config.design-tokens.yml.
Output a shadow value from the box-shadow token list.
box-shadow: gesso-box-shadow(1);Output a size value from the breakpoints token list.
@include breakpoint(gesso-breakpoint(desktop)) {
display: flex;
}
@include breakpoint-max(gesso-breakpoint(mobile), true) {
display: none;
}
@include breakpoint-min-max(
gesso-breakpoint(mobile),
gesso-breakpoint(tablet),
true
) {
display: block;
}Output a color value from the palette brand token list.
color: gesso-brand(cardinal, light);Output a color value from the colors token list.
color: gesso-color(text, primary);Output a size value from the constrains token list.
max-width: gesso-constrain(sm);Output a timing value from the transitions duration token list.
transition-duration: gesso-duration(short);Output an easing value from the transitions ease token list.
transition-timing-function: gesso-easing(ease-in-out);Output a stack value from the font-family token list.
font-family: gesso-font-family(primary);Output a size value from the font-size token list.
font-size: rem(gesso-font-size(2));Output a weight value from the font-weight token list.
font-weight: gesso-font-weight(semibold);Output a color value from the palette grayscale token list.
color: gesso-grayscale(gray-2);Output a height value from the line-height token list.
line-height: gesso-line-height(tight);Output a size value from the spacing token list.
margin-bottom: rem(gesso-spacing(4));Output an index value from the z-index token list.
z-index: gesso-z-index(modal);The values in the design tokens configuration file are also exported to
JavaScript objects so that the same values can be used in CSS and JS. The JS
objects can be found in source/00-config/_GESSO.es6.js (the filename is
inherited from upstream Gesso and deliberately left alone). This generated file
is gitignored and is rebuilt whenever npm run start, npm run build,
npm run watch or npm run dev are run.
For example, to use a breakpoint in a script:
import { BREAKPOINTS } from '../../../00-config/_GESSO.es6';
if (window.matchMedia(`min-width: ${BREAKPOINTS.desktop}`).matches) {
// Some script that should only run on larger screens.
}This will use the same breakpoint as breakpoint(gesso-breakpoint(desktop)) in
your Sass.
The theme uses custom mixins to specify viewport width based media queries:
breakpoint: min-width queriesbreakpoint-max: max-width queriesbreakpoint-min-max: queries with both a min and max width
Each mixin takes one or two width parameters, which can be a straight value
(e.g., 800px, 40em) or a design token value called using the gesso-breakpoint
function (e.g., gesso-breakpoint(tablet-lg)). The breakpoint-max and
breakpoint-min-max mixins can also take an optional parameter to subtract one
pixel from the max-width value, which can be useful when you want your query to
go up to the value but not to include it, such as when using breakpoint token
values.
Output a min-width based media query.
@include breakpoint(800px) {
display: flex;
}
@include breakpoint(gesso-breakpoint(desktop)) {
display: none;
}Output a max-width based media query. The optional $subtract_1_from_max
parameter will subtract 1px from the width value if set to true (default:
false).
@include breakpoint-max(900px) {
display: block;
}
@include breakpoint-max(gesso-breakpoint(mobile), true) {
display: none;
}Output a media query with both a min-width and max-width. The optional
$subtract_1_from_max parameter will subtract 1px from the max-width value if
set to true (default: false).
@include breakpoint-min-max(400px, 700px) {
display: flex;
}
@include breakpoint-min-max(
gesso-breakpoint(mobile),
gesso-breakpoint(tablet),
true
) {
display: block;
}This theme includes some additional filters and functions that can be used in
Twig templates. In Storybook they are registered in .storybook/preview.js from
the implementations in lib/. In Drupal they are provided by contrib modules or
by the SLAC Helper (slac_helper) module, as noted for each filter below.
Fork of Drupal Pattern Lab's add_attribute Twig function.
Allows Twig templates to add attributes that, in Drupal, will be merged with the
Drupal attributes object while also rendering in Storybook. Storybook
implementation: lib/addAttributesTwigExtension.js.
<div {{ add_attributes(
{
class: 'your-class-one your-class-two',
'data-foo': 'bar'
}
) }}>...</div>Twig filter to sort an object by key alphabetically. Storybook implementation:
lib/keysort.js.
{% for key, value in your_object|keysort %}
...
{% endfor %}Twig filter that turns a string into a value safe to use as an HTML id. This
theme's templates use unique_id, with 28 call sites across 20 Twig files.
Storybook implementation: lib/uniqueId.js, registered in .storybook/preview.js.
In Drupal it comes from the SLAC Helper (slac_helper) module.
Upstream Gesso renamed its filter to clean_unique_id at 5.4.0 and reverted
the rename at 5.4.5. This theme never followed it (it would have needed a matching
slac_helper change and every template call site at once), so there is nothing
to track.
{% set section_id = 'accordion-section'|unique_id %}Twig filter to get the rendered value of a field without its wrapper markup. In
Storybook it is provided by lib/fieldValue.js; in Drupal it is provided by the
Twig Field Value module (see
the Installation section above). Note that using |field_value breaks Drupal's
QuickEdit functionality.
{{ content.field_example|field_value }}Twig filter to transform a heading tag to the next level down (h2 -> h3, h3 -> h4, etc.) Used when the parent heading level can vary but, to maintain accessibility, the component's heading or subheading should change accordingly.
{% set subheading_element = title_element|subheading_level %}
<{{ subheading_element|default('h3') }}>...</{{ subheading_element|default('h3') }}>Not available yet, in Storybook or Drupal. Upstream Gesso ships both halves (
lib/subheadingLevelTwigExtension.jsand a PHP extension in its helper module). This theme takes neither untilslac_helperprovides the Drupal filter, so that a template never renders in one and fails in the other. Do not use|subheading_levelyet.
A static Storybook site can be built with npm run build-storybook, which
builds the theme assets first and then outputs Storybook to storybook/ in the
theme root.
storybook/ is gitignored — it is a build artifact and is never committed.
The published demo site is built in CI instead: the
.github/workflows/publish-demo-site.yml workflow runs npm ci and
npm run build-storybook on every push to main (and on manual dispatch), then
deploys storybook/ to this repository's GitHub Pages site.
The demo site is published at https://slac.github.io/slac-drupal-profile-theme/.
Some aspects of the theme can be configured on the theme settings page
(Appearance → SLAC → Settings). The form is built in theme-settings.php
and the values are declared in config/schema/slac.schema.yml.
Back to Top
include_back_to_top— whether to include the Back to Top component (default: on).threshold— how far, in pixels, a user should scroll down the page before the Back to Top component appears (default: 200).smooth_scroll— whether to animate the scroll back to the top (default: on).
Breadcrumb
include_current_page_in_breadcrumb— whether the current page is included as the last breadcrumb item (default: on).
Hide Social Share Icons
hide_social_media_share_icons— if enabled, social media icons will not be shown on the side of the page.
SLAC Today header link
slac_today_header_link— the URL used for the SLAC Today link in the site header.
SLAC search
include_slac_web_search— whether to offer the SLAC-wide web search option in the search form (default: off).search_this_site_placeholder— custom placeholder text shown when the "This site" search option is selected. If left empty, the Organization Acronym, then the Organization Name, then the Site Name is used.
The list above is the complete set — anything not listed is not a setting of this theme. In particular, upstream Gesso's Button styles theme setting and its Gesso Button link-field formatter are not part of this theme.
This theme began as a copy of Gesso and
still shares its build toolchain, Sass architecture, gesso-* design-token
accessor functions, and Storybook integration. The rename from gesso to slac
happened long ago; there is nothing left to rename.
- The theme currently tracks Gesso 5.4.6, which is the version recorded in
package.json. - Upstream toolchain changes (Node, webpack, ESLint, Stylelint, Storybook, TypeScript, Sass module-system migration) are merged in.
- Upstream component, template, and design-token changes are deliberately not merged. SLAC's components and tokens have diverged and are maintained here.
- Gesso 5.4.6 is the baseline for future merges. It was reached one
upstream release at a time (the records are in
.claude/:gesso-STATE.md, the per-release plans in.claude/gesso-plans/, andgesso-review-flags.md). Every deliberate deviation from upstream is listed in.claude/gesso-deviations.mdwith its reason; in code it carries a one-line// Local: …; see .claude/gesso-deviations.mdmarker (for example thejqueryexternal and the Stylelintfilesscope inwebpack.common.js). Keep that convention: add a register row, and a one-line marker at the site of the deviation. - A few upstream identifiers are retained on purpose, because renaming them
would touch every SCSS and JS file for no functional gain: the
gesso-*Sass function prefix, andsource/00-config/_GESSO.es6.js.
Please use this repository's GitHub issue queue for discussion, bug reports,
feature requests, etc. Pull requests should target main.
Releases are cut by pushing a tag. The
.github/workflows/build-assets.yml workflow then:
- Checks out the tag, runs
npm ciandnpm run build, and removesnode_modules. - Writes the tag name into
slac.info.ymlas theversionproperty. - Zips the result as
slac.zip, excluding VCS files,node_modules/,.editorconfigand.claude/.source/ships: templates include@components/...from it. A step then checks the zip's contents. - Uploads
slac.zipto the GitHub Release for that tag. - Once the build job has succeeded, sends a
repository_dispatch(release-published) to theslac-it/slac-drupal-satisrepository, so that Satis picks up the new version and Composer consumers can require it.
There is no need to commit build artifacts: dist/css, dist/js, the SVG
sprite, the generated design-token partials and storybook/ are all gitignored
and produced by CI.
This theme is maintained by the SLAC web team.
It is derived from the Gesso theme by Forum One, which is maintained by Corey Lafferty, KJ Monahan, Dan Mouyard (@dcmouyard), and Tommy Alter.