Skip to content
Star on GitHub
CollaborationBanner
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Banner

An announcement on a tint of its tone, with its own line drawing. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add banner
New

Memory that carries across threads

Agents keep what they learn about your project, so you only explain things once.

01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add banner

React

$npx designme add banner --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Banner puts one announcement in front of people: an eyebrow, a title, a line of text, a primary action and a quieter link, on a tint of its tone, with a line drawing on the right. It comes with four drawings of its own.

  • When a new feature or a beta is worth a moment of attention, like "Memory that carries across threads" with Turn on memory.
  • When a guide or a setup flow should be one click away, without a modal in the way.
  • When people need a heads-up, such as planned maintenance, while it still applies.
  • When a page has room for one message above its content.
03

Usage

Import it and drive it from your own state.

Import

import { Banner } from '@/components/designme/banner/Banner';

Example

The demo above as a React component, 37 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useRef, useState } from 'react';
import { Banner, type BannerArtName, type BannerTone } from './Banner';
import { useStory } from '../lib/useScript';

/* ---------- Banner: three announcements one after another, each with its own tone and drawing, then the last one is
   dismissed and brought back with Undo ---------- */
const BANNERS: { tone: BannerTone; art: BannerArtName; eyebrow: string; title: string; description: string; action: string; secondary: string }[] = [
  { tone: 'accent', art: 'layers', eyebrow: 'New', title: 'Memory that carries across threads', description: 'Agents keep what they learn about your project, so you only explain things once.', action: 'Turn on memory', secondary: 'What it keeps' },
  { tone: 'violet', art: 'tools', eyebrow: 'Beta', title: 'Connect your tools in one place', description: 'Linear, GitHub and Slack in one list, with access you can scope for each agent.', action: 'Connect a tool', secondary: 'Read the docs' },
  { tone: 'green', art: 'steps', eyebrow: 'Guide', title: 'Set up your first automation', description: 'Five short steps, about ten minutes. The agent can do most of them for you.', action: 'Start the guide', secondary: 'Maybe later' },
  { tone: 'amber', art: 'path', eyebrow: 'Heads-up', title: 'Maintenance on Sunday, 02:00 UTC', description: 'Agents pause for about an hour, and queued runs pick up where they left off.', action: 'Add to calendar', secondary: 'Status page' }
];

export default function BannerDemo() {
  const ref = useRef<HTMLDivElement>(null);
  const [i, setI] = useState(0);
  const [open, setOpen] = useState(true);
  // the preview's story: the next two banners take the first one's place, then the last is dismissed and comes back with
  // Undo. Your first click, key or focus stops it, and a mouse resting on it holds it where it is
  useStory(ref, async ({ wait, press }) => {
    await wait(4000); setI(1);
    await wait(4000); setI(2);
    await wait(4000); press('.dm-bnx-x');
    await wait(3000); press('.dm-bnx-undo');
  });
  const b = BANNERS[i];
  return (
    <div ref={ref} style={{ width: '100%' }}>
      <Banner tone={b.tone} art={b.art} eyebrow={b.eyebrow} title={b.title} description={b.description}
        action={{ label: b.action, onClick: () => {} }} secondary={{ label: b.secondary, onClick: () => {} }}
        open={open} onOpenChange={setOpen} />
    </div>
  );
}
04

Props

Everything Banner takes.

PropTypeWhat it does
toneBannerToneThe tint, and the colour the eyebrow and the drawing take. accent by default
artBannerArtName | ReactElement | nullOne of the four drawings, or your own SVG (240 by 160 is the size it is shown at). null for none. layers by default
eyebrowstringA short label over the title, like New or Beta
titlerequiredReactNode
descriptionReactNodeOne line or two under the title
actionBannerActionThe primary button, on the accent
secondaryBannerActionA quieter link beside it
onDismiss() => voidCalled by the dismiss button
dismissiblebooleanShows the dismiss button. true by default
openbooleanWhether the banner is showing (controlled), with defaultOpen and onOpenChange. Dismiss sets it to false, Undo to true
defaultOpenboolean
onOpenChange(open: boolean) => void
undoboolean | stringAfter a dismiss, a quiet line with Undo keeps the banner's place: true says "Banner dismissed", a string says something else, and false leaves nothing. true by default
labelstringThe banner's name for screen readers. Announcement by default
classNamestring
05

Code

The full source, as the CLI installs it.

Banner.tsxReact · 143 lines
'use client';
import { useEffect, useRef, useState, type CSSProperties, type ReactElement, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useReducedMotion } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { Swap, Tip } from '../lib/motion';
import '../designme.css';
import './banner.css';

/** The tint the banner sits on. `accent` is the accent's own tint; the others are tints of the tag colours */
export type BannerTone = 'accent' | 'neutral' | 'blue' | 'violet' | 'green' | 'amber';
/** The four drawings that come with it */
export type BannerArtName = 'layers' | 'steps' | 'tools' | 'path';

export interface BannerAction {
  label: string;
  onClick?: () => void;
  /** Makes it a link instead of a button */
  href?: string;
}

export interface BannerProps {
  /** The tint, and the colour the eyebrow and the drawing take. accent by default */
  tone?: BannerTone;
  /** One of the four drawings, or your own SVG (240 by 160 is the size it is shown at). null for none. layers by default */
  art?: BannerArtName | ReactElement | null;
  /** A short label over the title, like New or Beta */
  eyebrow?: string;
  title: ReactNode;
  /** One line or two under the title */
  description?: ReactNode;
  /** The primary button, on the accent */
  action?: BannerAction;
  /** A quieter link beside it */
  secondary?: BannerAction;
  /** Called by the dismiss button */
  onDismiss?: () => void;
  /** Shows the dismiss button. true by default */
  dismissible?: boolean;
  /** Whether the banner is showing (controlled), with defaultOpen and onOpenChange. Dismiss sets it to false, Undo to true */
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  /** After a dismiss, a quiet line with Undo keeps the banner's place: true says "Banner dismissed", a string says something
      else, and false leaves nothing. true by default */
  undo?: boolean | string;
  /** The banner's name for screen readers. Announcement by default */
  label?: string;
  className?: string;
}

// the order each line draws in, and how far a part moves under the pointer
const d = (n: number) => ({ '--dm-d': n }) as CSSProperties;
const z = (n: number) => ({ '--dm-z': n }) as CSSProperties;
/* each drawing is 240 by 160, written as plain paths, all in one isometric view on a floor grid, with their drawn box
   centred in it: layers (three plates of a stack, pulled apart, the bottom one standing on the floor), steps (a staircase climbing to a flag, on a floor that
   follows its footprint), tools (five tool tiles, Slack, GitHub, Linear, Gmail and Apollo, each with its mark on top) and
   path (a dotted route across the floor to a raised tile). dm-ln lines draw themselves in, dm-gd are the dashed
   construction lines and the floor, which fade in, and dm-pl faces wear the banner's fill so what is in front hides what
   is behind */
const ART: Record<BannerArtName, ReactElement> = {
  layers: <><g className="dm-gds"><path className="dm-gd" d="M120 51L36.9 99M140.8 63L57.6 111M161.6 75L78.4 123M182.4 87L99.2 135M203.1 99L120 147M120 51L203.1 99M99.2 63L182.4 111M78.4 75L161.6 123M57.6 87L140.8 135M36.9 99L120 147"/></g><g className="dm-gds"><path className="dm-gd" d="M78.4 43L78.4 93M161.6 43L161.6 93M120 67L120 117"/></g><g className="dm-z" style={z(0)}><path className="dm-pl" d="M120 69L161.6 93L161.6 99L120 123L78.4 99L78.4 93Z"/><path className="dm-ln" style={d(0)} pathLength={1} d="M120 69L161.6 93L120 117L78.4 93Z"/><path className="dm-ln" style={d(1)} pathLength={1} d="M161.6 93L161.6 99L120 123L78.4 99L78.4 93"/><path className="dm-ln" style={d(2)} pathLength={1} d="M120 117L120 123"/></g><g className="dm-z" style={z(1)}><path className="dm-pl" d="M120 41L161.6 65L161.6 71L120 95L78.4 71L78.4 65Z"/><path className="dm-ln" style={d(3)} pathLength={1} d="M120 41L161.6 65L120 89L78.4 65Z"/><path className="dm-ln" style={d(4)} pathLength={1} d="M161.6 65L161.6 71L120 95L78.4 71L78.4 65"/><path className="dm-ln" style={d(5)} pathLength={1} d="M120 89L120 95"/></g><g className="dm-z" style={z(2)}><path className="dm-pl" d="M120 13L161.6 37L161.6 43L120 67L78.4 43L78.4 37Z"/><path className="dm-ln" style={d(6)} pathLength={1} d="M120 13L161.6 37L120 61L78.4 37Z"/><path className="dm-ln" style={d(7)} pathLength={1} d="M161.6 37L161.6 43L120 67L78.4 43L78.4 37"/><path className="dm-ln" style={d(8)} pathLength={1} d="M120 61L120 67"/><path className="dm-ln dm-ac" style={d(9)} pathLength={1} d="M120 23.8L142.9 37L120 50.2L97.1 37Z"/></g></>,
  steps: <><g className="dm-gds"><path className="dm-gd" d="M135.6 59L42.1 113M151.2 68L57.6 122M166.8 77L73.2 131M182.4 86L88.8 140M197.9 95L104.4 149M135.6 59L197.9 95M120 68L182.4 104M104.4 77L166.8 113M88.8 86L151.2 122M73.2 95L135.6 131M57.6 104L120 140M42.1 113L104.4 149"/></g><g className="dm-z" style={z(0)}><path className="dm-pl" d="M135.6 21L166.8 39L166.8 95L151.2 104L120 86L120 30Z"/><path className="dm-ln" style={d(0)} pathLength={1} d="M135.6 21L166.8 39L151.2 48L120 30Z"/><path className="dm-ln" style={d(1)} pathLength={1} d="M166.8 39L166.8 95L151.2 104L120 86L120 30"/><path className="dm-ln" style={d(2)} pathLength={1} d="M151.2 48L151.2 104"/><path className="dm-pl" d="M120 44L151.2 62L151.2 104L135.6 113L104.4 95L104.4 53Z"/><path className="dm-ln" style={d(3)} pathLength={1} d="M120 44L151.2 62L135.6 71L104.4 53Z"/><path className="dm-ln" style={d(4)} pathLength={1} d="M151.2 62L151.2 104L135.6 113L104.4 95L104.4 53"/><path className="dm-ln" style={d(5)} pathLength={1} d="M135.6 71L135.6 113"/><path className="dm-pl" d="M104.4 67L135.6 85L135.6 113L120 122L88.8 104L88.8 76Z"/><path className="dm-ln" style={d(6)} pathLength={1} d="M104.4 67L135.6 85L120 94L88.8 76Z"/><path className="dm-ln" style={d(7)} pathLength={1} d="M135.6 85L135.6 113L120 122L88.8 104L88.8 76"/><path className="dm-ln" style={d(8)} pathLength={1} d="M120 94L120 122"/><path className="dm-pl" d="M88.8 90L120 108L120 122L104.4 131L73.2 113L73.2 99Z"/><path className="dm-ln" style={d(9)} pathLength={1} d="M88.8 90L120 108L104.4 117L73.2 99Z"/><path className="dm-ln" style={d(10)} pathLength={1} d="M120 108L120 122L104.4 131L73.2 113L73.2 99"/><path className="dm-ln" style={d(11)} pathLength={1} d="M104.4 117L104.4 131"/></g><g className="dm-z" style={z(2)}><path className="dm-ln dm-ac" style={d(12)} pathLength={1} d="M137.1 30.9V8.9h14l-4 5 4 5h-14"/></g></>,
  tools: <><g className="dm-gds"><path className="dm-gd" d="M120 30.5L26.5 84.5M151.2 48.5L57.6 102.5M182.4 66.5L88.8 120.5M213.5 84.5L120 138.5M120 30.5L213.5 84.5M88.8 48.5L182.4 102.5M57.6 66.5L151.2 120.5M26.5 84.5L120 138.5"/></g><g className="dm-z" style={z(1)}><path className="dm-pl" d="M120 21.5L142.5 34.5L142.5 48.5L120 61.5L97.5 48.5L97.5 34.5Z"/><path className="dm-ln" style={d(0)} pathLength={1} d="M120 21.5L142.5 34.5L120 47.5L97.5 34.5Z"/><path className="dm-ln" style={d(1)} pathLength={1} d="M142.5 34.5L142.5 48.5L120 61.5L97.5 48.5L97.5 34.5"/><path className="dm-ln" style={d(2)} pathLength={1} d="M120 47.5L120 61.5"/><g className="dm-lg" transform="matrix(0.613 -0.354 0.613 0.354 105.278 34.5)"><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/></g><path className="dm-pl" d="M57.6 57.5L80.2 70.5L80.2 84.5L57.6 97.5L35.1 84.5L35.1 70.5Z"/><path className="dm-ln" style={d(3)} pathLength={1} d="M57.6 57.5L80.2 70.5L57.6 83.5L35.1 70.5Z"/><path className="dm-ln" style={d(4)} pathLength={1} d="M80.2 70.5L80.2 84.5L57.6 97.5L35.1 84.5L35.1 70.5"/><path className="dm-ln" style={d(5)} pathLength={1} d="M57.6 83.5L57.6 97.5"/><g className="dm-lg" transform="matrix(0.613 -0.354 0.613 0.354 42.924 70.5)"><path d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z"/></g><path className="dm-pl" d="M120 57.5L142.5 70.5L142.5 84.5L120 97.5L97.5 84.5L97.5 70.5Z"/><path className="dm-ln" style={d(6)} pathLength={1} d="M120 57.5L142.5 70.5L120 83.5L97.5 70.5Z"/><path className="dm-ln" style={d(7)} pathLength={1} d="M142.5 70.5L142.5 84.5L120 97.5L97.5 84.5L97.5 70.5"/><path className="dm-ln" style={d(8)} pathLength={1} d="M120 83.5L120 97.5"/><g className="dm-lg" transform="matrix(0.613 -0.354 0.613 0.354 105.278 70.5)"><path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z"/></g><path className="dm-pl" d="M182.4 57.5L204.9 70.5L204.9 84.5L182.4 97.5L159.8 84.5L159.8 70.5Z"/><path className="dm-ln" style={d(9)} pathLength={1} d="M182.4 57.5L204.9 70.5L182.4 83.5L159.8 70.5Z"/><path className="dm-ln" style={d(10)} pathLength={1} d="M204.9 70.5L204.9 84.5L182.4 97.5L159.8 84.5L159.8 70.5"/><path className="dm-ln" style={d(11)} pathLength={1} d="M182.4 83.5L182.4 97.5"/><g className="dm-lg" transform="matrix(0.613 -0.354 0.613 0.354 167.631 70.5)"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></g><path className="dm-pl" d="M120 93.5L142.5 106.5L142.5 120.5L120 133.5L97.5 120.5L97.5 106.5Z"/><path className="dm-ln" style={d(12)} pathLength={1} d="M120 93.5L142.5 106.5L120 119.5L97.5 106.5Z"/><path className="dm-ln" style={d(13)} pathLength={1} d="M142.5 106.5L142.5 120.5L120 133.5L97.5 120.5L97.5 106.5"/><path className="dm-ln" style={d(14)} pathLength={1} d="M120 119.5L120 133.5"/><g className="dm-lg" transform="matrix(0.613 -0.354 0.613 0.354 105.278 106.5)"><g transform="scale(.663)"><path d="M19.5993 0.0862365L19.605 13.2568C19.6058 15.3375 17.4222 16.6715 15.6079 15.6986L2.58376 8.7153C3.57706 7.05795 4.82616 5.57609 6.27427 4.32386L16.489 13.8945C17.0303 14.4015 17.8835 13.8518 17.6605 13.1398L13.6992 0.493553C15.0326 0.17147 16.4233 0 17.8536 0C18.4428 0 19.0248 0.0296814 19.5993 0.0862365Z"/><path d="M16.0635 36.1087L16.0578 23.0046C16.057 20.9239 18.2407 19.5898 20.0549 20.5627L33.0838 27.5486C32.0838 29.2016 30.8289 30.6786 29.3751 31.925L19.1738 22.3668C18.6326 21.8598 17.7793 22.4095 18.0023 23.1215L21.9486 35.72C20.6338 36.0329 19.263 36.1989 17.8539 36.1989C17.2497 36.1989 16.6523 36.1683 16.0635 36.1087Z"/><path d="M22.0105 16.77L31.4705 6.39392C30.2362 4.92008 28.7742 3.6486 27.1384 2.63702L20.2306 15.8767C19.2709 17.716 20.5871 19.9298 22.6396 19.9288L35.6183 19.923C35.6775 19.3234 35.7082 18.7151 35.7082 18.0996C35.7082 16.6683 35.5436 15.2761 35.2338 13.9406L22.7549 17.9576C22.0526 18.1837 21.5103 17.3187 22.0105 16.77Z"/><path d="M0.0842758 16.3383L13.0237 16.3325C15.0764 16.3317 16.3923 18.5454 15.4327 20.3846L8.56047 33.5561C6.93095 32.547 5.47394 31.2801 4.24344 29.8121L13.653 19.4914C14.1531 18.9427 13.6107 18.0777 12.9084 18.3037L0.485078 22.3029C0.168551 20.954 0 19.5467 0 18.0994C0 17.5051 0.0290814 16.9177 0.0842758 16.3383Z"/></g></g></g></>,
  path: <><g className="dm-gds"><path className="dm-gd" d="M120 26L26.5 80M143.4 39.5L49.9 93.5M166.8 53L73.2 107M190.1 66.5L96.6 120.5M213.5 80L120 134M120 26L213.5 80M96.6 39.5L190.1 93.5M73.2 53L166.8 107M49.9 66.5L143.4 120.5M26.5 80L120 134"/></g><g className="dm-z" style={z(0)}><path className="dm-dt" d="M49.9 80L73.2 93.5L120 66.5L166.8 93.5L190.1 80"/><path className="dm-ln" style={d(0)} pathLength={1} d="M49.9 74L60.2 80L49.9 86L39.5 80Z"/></g><g className="dm-z" style={z(2)}><path className="dm-pl" d="M190.1 51L205.7 60L205.7 80L190.1 89L174.6 80L174.6 60Z"/><path className="dm-ln dm-ac" style={d(1)} pathLength={1} d="M190.1 51L205.7 60L190.1 69L174.6 60Z"/><path className="dm-ln" style={d(2)} pathLength={1} d="M205.7 60L205.7 80L190.1 89L174.6 80L174.6 60"/><path className="dm-ln" style={d(3)} pathLength={1} d="M190.1 69L190.1 89"/></g></>
};

function Act({ a, className }: { a: BannerAction; className: string }) {
  return a.href
    ? <a className={className} href={a.href} onClick={a.onClick}><span>{a.label}</span></a>
    : <button type="button" className={className} onClick={a.onClick}><span>{a.label}</span></button>;
}
// text that is a string crossfades when it changes, as on the site
const T = (n: ReactNode) => (typeof n === 'string' ? <Swap>{n}</Swap> : n);

/** An announcement on a tint of its tone: an eyebrow, a title, a line of text and its actions, a line drawing on the right
    (a strip across the top in a narrow column), and a dismiss with Undo. */
export function Banner({ tone = 'accent', art = 'layers', eyebrow, title, description, action, secondary, onDismiss, dismissible = true,
  open: openProp, defaultOpen = true, onOpenChange, undo = true, label = 'Announcement', className }: BannerProps) {
  const reduce = useReducedMotion();
  const [open, setOpen] = useControllable(openProp, defaultOpen, onOpenChange);
  // the banner leaving, and the height it had, which the Undo line keeps so nothing below moves
  const [leaving, setLeaving] = useState(false);
  const [held, setHeld] = useState<number | null>(null);
  const slot = useRef<HTMLDivElement>(null), xRef = useRef<HTMLButtonElement>(null), undoRef = useRef<HTMLButtonElement>(null);
  const focus = useRef<'undo' | 'x' | null>(null);
  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
  useEffect(() => () => clearTimeout(timer.current), []);
  useEffect(() => { if (open) { setLeaving(false); setHeld(null); } }, [open]);
  // the focus follows the button that went: to Undo after a dismiss, back to Dismiss after Undo
  useEffect(() => {
    if (focus.current === 'undo' && !open) undoRef.current?.focus({ preventScroll: true });
    if (focus.current === 'x' && open) xRef.current?.focus({ preventScroll: true });
    focus.current = null;
  }, [open]);

  const dismiss = () => {
    if (leaving) return;
    if (slot.current) setHeld(Math.ceil(slot.current.getBoundingClientRect().height));
    if (slot.current?.contains(document.activeElement)) focus.current = 'undo';
    setLeaving(true); onDismiss?.();
    timer.current = setTimeout(() => setOpen(false), reduce ? 0 : 320);
  };
  const restore = () => { if (slot.current?.contains(document.activeElement)) focus.current = 'x'; setOpen(true); };

  const name = typeof art === 'string' ? art : null;
  const gone = !open && undo !== false;
  if (!open && !gone) return null;
  return (
    <div ref={slot} data-dm="" className={cx('dm-bnx-slot', held !== null && 'dm-held', className)} style={held !== null ? { minHeight: held } : undefined}>
      {open && (
        <div className={cx('dm-bnx', leaving && 'dm-leave')} role="region" aria-label={label} data-tone={tone} data-art={name ?? undefined}>
          {art && (
            <div className="dm-bnx-art" aria-hidden="true">
              {name
                ? <svg key={name} className="dm-bnx-svg" viewBox="0 0 240 160" width="240" height="160" focusable="false">{ART[name]}</svg>
                : <div className="dm-bnx-svg">{art}</div>}
            </div>
          )}
          <div className="dm-bnx-tx">
            {eyebrow && <span className="dm-bnx-eb">{T(eyebrow)}</span>}
            <p className="dm-bnx-t">{T(title)}</p>
            {description && <p className="dm-bnx-d">{T(description)}</p>}
            {(action || secondary) && (
              <div className="dm-bnx-acts">
                {action && <Act a={action} className="dm-pill dm-primary dm-sm dm-bnx-go" />}
                {secondary && <Act a={secondary} className="dm-bnx-lk" />}
              </div>
            )}
          </div>
          {dismissible && <Tip label="Dismiss"><button ref={xRef} type="button" className="dm-ib dm-bnx-x" aria-label="Dismiss" onClick={dismiss}><Icon name="x" /></button></Tip>}
        </div>
      )}
      {gone && (
        <div className="dm-bnx-gone dm-enter" role="status">
          <span>{typeof undo === 'string' ? undo : 'Banner dismissed'}</span>
          <button ref={undoRef} type="button" className="dm-pill dm-ghost dm-sm dm-bnx-undo" onClick={restore}>Undo</button>
        </div>
      )}
    </div>
  );
}
banner.cssReact · 78 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Banner ---------- */
/* an announcement on a tint of its tone: an eyebrow, a title, a line of text and its actions on the left, and one of four line
   drawings on the right, drawn as a hairline with a few construction lines, the way Blueprint draws everything. Each tone
   names three colours: --bnx-m draws the art (3:1 on the fill), --bnx-c writes the eyebrow (4.5:1) and --bnx-f is what
   the fill is a tint of. The fill is solid, so the close button and the art's own faces can wear it too. It has no border:
   the tint is its edge */
.dm-bnx-slot{width:100%}
.dm-bnx{position:relative;width:100%;min-height:176px;display:flex;flex-direction:column;justify-content:center;overflow:hidden;border-radius:var(--dm-r-card);container:bnx/inline-size;isolation:isolate;
  --dm-bnx-m:var(--dm-accent-mark);--dm-bnx-c:var(--dm-accent-fg);--dm-bnx-f:var(--dm-accent);--dm-bnx-k:10%;--dm-bnx-ka:var(--dm-bnx-k);
  --dm-bnx-bg:color-mix(in srgb,var(--dm-bnx-f) var(--dm-bnx-ka),var(--dm-surface));
  background:var(--dm-bnx-bg);transition:background-color var(--dm-t-slow) var(--dm-ease)}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-bnx{--dm-bnx-k:14%}}
:root[data-theme="dark"] .dm-bnx{--dm-bnx-k:14%}
.dm-bnx[data-tone="neutral"]{--dm-bnx-m:var(--dm-muted);--dm-bnx-c:var(--dm-fg2);--dm-bnx-bg:var(--dm-chip)}
.dm-bnx[data-tone="blue"]{--dm-bnx-m:var(--dm-pb);--dm-bnx-c:var(--dm-pb);--dm-bnx-f:var(--dm-pb);--dm-bnx-ka:var(--dm-bnx-k)}
.dm-bnx[data-tone="violet"]{--dm-bnx-m:var(--dm-pl);--dm-bnx-c:var(--dm-pl);--dm-bnx-f:var(--dm-pl);--dm-bnx-ka:var(--dm-bnx-k)}
.dm-bnx[data-tone="green"]{--dm-bnx-m:var(--dm-pm);--dm-bnx-c:var(--dm-pm);--dm-bnx-f:var(--dm-pm);--dm-bnx-ka:var(--dm-bnx-k)}
.dm-bnx[data-tone="amber"]{--dm-bnx-m:var(--dm-py);--dm-bnx-c:var(--dm-py);--dm-bnx-f:var(--dm-py);--dm-bnx-ka:var(--dm-bnx-k)}
/* the text keeps clear of the art: its right padding is the art's width */
.dm-bnx-tx{position:relative;z-index:1;display:flex;flex-direction:column;align-items:flex-start;padding:var(--dm-s-5) calc(min(42%,300px) + var(--dm-s-2)) var(--dm-s-5) var(--dm-s-6)}
.dm-bnx-eb{font-size:12px;font-weight:500;line-height:16px;color:var(--dm-bnx-c);transition:color var(--dm-t-slow) var(--dm-ease)}
.dm-bnx-t{margin:var(--dm-s-2) 0 0;font-size:14px;font-weight:500;line-height:20px;color:var(--dm-fg);text-wrap:balance}
.dm-bnx-d{margin:var(--dm-s-1) 0 0;font-size:13px;line-height:20px;color:var(--dm-fg2);text-wrap:pretty}
.dm-bnx-acts{display:flex;flex-wrap:wrap;align-items:center;gap:var(--dm-s-2) var(--dm-s-4);margin-top:var(--dm-s-4)}
.dm-bnx-lk{display:inline-flex;align-items:center;height:28px;font-size:13px;color:var(--dm-fg);text-decoration:underline;text-decoration-color:color-mix(in srgb,currentColor 35%,transparent);text-underline-offset:3px;transition:text-decoration-color var(--dm-t-fast)}
@media (hover:hover) and (pointer:fine){.dm-bnx-lk:hover{text-decoration-color:currentColor}}
/* on a coloured banner the primary button is white in light mode, with the text colour on it, so it stands out from the tint
   in every tone; in dark mode, where the tint is deep, it stays the accent. A neutral (grey) banner keeps the accent */
.dm-bnx:not([data-tone="neutral"]) .dm-bnx-go{background:var(--dm-surface);color:var(--dm-fg);box-shadow:var(--dm-shadow)}
@media (hover:hover) and (pointer:fine){.dm-bnx:not([data-tone="neutral"]) .dm-bnx-go:hover{background:var(--dm-surface);box-shadow:var(--dm-shadow-pop)}}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .dm-bnx .dm-bnx-go{background:var(--dm-accent);color:var(--dm-on-accent);box-shadow:none}
  @media (hover:hover) and (pointer:fine){:root:not([data-theme="light"]) .dm-bnx .dm-bnx-go:hover{background:var(--dm-accent-2);box-shadow:none}}}
:root[data-theme="dark"] .dm-bnx .dm-bnx-go{background:var(--dm-accent);color:var(--dm-on-accent);box-shadow:none}
@media (hover:hover) and (pointer:fine){:root[data-theme="dark"] .dm-bnx .dm-bnx-go:hover{background:var(--dm-accent-2);box-shadow:none}}
/* an optional close button in the top right corner (the card here leaves it out) */
.dm-bnx-x{position:absolute;top:var(--dm-s-2);right:var(--dm-s-2);z-index:2;width:28px;height:28px;background:var(--dm-bnx-bg)}
@media (hover:hover) and (pointer:fine){.dm-bnx-x:hover{background:color-mix(in srgb,var(--dm-fg) 8%,var(--dm-bnx-bg))}}
/* the art: a 240 by 160 drawing at its own size in the middle of the area on the right, cropped by it and faded in from the
   text's side, so a narrower banner shows less of it rather than a smaller one */
.dm-bnx-art{position:absolute;top:0;right:0;bottom:0;width:min(42%,300px);overflow:hidden;pointer-events:none;-webkit-mask-image:linear-gradient(90deg,transparent,#000 var(--dm-s-8));mask-image:linear-gradient(90deg,transparent,#000 var(--dm-s-8))}
.dm-bnx-svg{position:absolute;left:50%;top:50%;width:240px;height:160px;margin:-80px 0 0 -120px;overflow:visible;display:block}
.dm-bnx-svg .dm-ln{fill:none;stroke:var(--dm-bnx-m);stroke-width:1;stroke-linejoin:round;stroke-dasharray:1 1;stroke-dashoffset:0;animation:dm-bnxdraw 1.1s var(--dm-ease) calc(160ms + var(--dm-d,0) * 60ms) backwards}
.dm-bnx-svg .dm-ac{stroke-width:1.5}
.dm-bnx-svg .dm-gd{fill:none;stroke:var(--dm-bnx-m);stroke-width:1;stroke-dasharray:2 3;opacity:.45;animation:dm-bnxfade .9s var(--dm-ease) 60ms backwards}
/* the faces under the lines wear the fill, so what is in front hides what is behind once it is drawn */
.dm-bnx-svg .dm-pl{fill:var(--dm-bnx-bg);stroke:none;animation:dm-bnxfade .6s var(--dm-ease) .5s backwards}
.dm-bnx-svg .dm-lg{fill:var(--dm-bnx-m);animation:dm-bnxfade .6s var(--dm-ease) .7s backwards}
.dm-bnx-svg .dm-dt{fill:none;stroke:var(--dm-bnx-m);stroke-width:2;stroke-linecap:round;stroke-dasharray:0 5;animation:dm-bnxfade .9s var(--dm-ease) .4s backwards}
@keyframes dm-bnxdraw{from{stroke-dashoffset:1}}
@keyframes dm-bnxfade{from{opacity:0}}
/* under the pointer the drawing pulls apart a little: each part moves by its depth (--z), with no blur */
.dm-bnx-svg .dm-z{transition:transform var(--dm-t-slow) var(--dm-ease)}
@media (hover:hover) and (pointer:fine){.dm-bnx:hover .dm-bnx-svg .dm-z{transform:translate(calc(var(--dm-z,0) * 1px),calc(var(--dm-z,0) * -2.5px))}}
/* the card: the banner with a switcher under it that flips through the four styles, the two kept together and centred */
.dm-dm.dm-bnx-dm{min-height:0;gap:var(--dm-s-5);justify-content:center}
.dm-dm.dm-bnx-dm>.dm-dm-main{margin-block:0}
/* a phone, or any narrow column: the art becomes a shorter strip across the top that fades out before the text starts,
   so nothing is drawn behind the words. The drawing is shown at six tenths, so all of it fits in the strip, with its lines
   thickened to stay a hairline */
@container bnx (max-width:479px){
  .dm-bnx-art{left:0;bottom:auto;width:auto;height:88px;-webkit-mask-image:linear-gradient(#000 68px,transparent 88px);mask-image:linear-gradient(#000 68px,transparent 88px)}
  .dm-bnx-svg{top:46px;transform:scale(.6)}
  .dm-bnx-svg :is(.dm-ln,.dm-gd){stroke-width:1.6}
  .dm-bnx-svg .dm-ac{stroke-width:2.4}
  .dm-bnx-svg .dm-dt{stroke-width:3.2}
  .dm-bnx-tx{padding:84px var(--dm-s-5) var(--dm-s-5)}
}
@media (prefers-reduced-motion:reduce){.dm-bnx-svg *{animation:none!important}.dm-bnx-svg .dm-z{transform:none!important}}
/* Blueprint: square, and the eyebrow as a small capital label */
:root:not([data-style="soft"]) .dm-bnx:is(:not([data-tone]),[data-tone="accent"]):not(:where([data-style="soft"],[data-style="soft"] *)){--dm-bnx-ka:20%}
@media (prefers-color-scheme:dark){:root:not([data-style="soft"]):not([data-theme="light"]) .dm-bnx:is(:not([data-tone]),[data-tone="accent"]):not(:where([data-style="soft"],[data-style="soft"] *)){--dm-bnx-ka:9%}}
:root:not([data-style="soft"])[data-theme="dark"] .dm-bnx:is(:not([data-tone]),[data-tone="accent"]):not(:where([data-style="soft"],[data-style="soft"] *)){--dm-bnx-ka:9%}
:root:not([data-style="soft"]) .dm-bnx-eb:not(:where([data-style="soft"],[data-style="soft"] *)){font-size:11px;letter-spacing:.06em;text-transform:uppercase}
/* Blueprint draws its lines with square ends, so the dotted route is square dots */
:root:not([data-style="soft"]) .dm-bnx-svg .dm-dt:not(:where([data-style="soft"],[data-style="soft"] *)){stroke-dasharray:2 3}
@container bnx (max-width:479px){:root:not([data-style="soft"]) .dm-bnx-svg .dm-dt:not(:where([data-style="soft"],[data-style="soft"] *)){stroke-dasharray:3.2 4.8}}
banner.htmlHTML · 18 lines
<div class="card wide" data-c="banner">
  <div class="stage-in">
    <div class="dm bnx-dm"><div class="dm-main bnx-slot">
      <div class="bnx" role="region" data-tone="accent" data-art="layers" aria-label="Announcement">
        <div class="bnx-art" aria-hidden="true"></div>
        <div class="bnx-tx">
          <span class="bnx-eb"><span>New</span></span>
          <p class="bnx-t"><span>Memory that carries across threads</span></p>
          <p class="bnx-d"><span>Agents keep what they learn about your project, so you only explain things once.</span></p>
          <div class="bnx-acts"><button class="pill primary sm bnx-go"><span>Turn on memory</span></button><button class="bnx-lk"><span>What it keeps</span></button></div>
        </div>
      </div>
    </div>
    <div class="demo"><div class="seg2" role="group" aria-label="Banner style"><button aria-pressed="true" data-v="0">Accent</button><button aria-pressed="false" data-v="1">Violet</button><button aria-pressed="false" data-v="2">Green</button><button aria-pressed="false" data-v="3">Amber</button></div></div>
    </div>
  </div>
</div>
banner.cssHTML · 77 lines
/* ---------- Banner ---------- */
/* an announcement on a tint of its tone: an eyebrow, a title, a line of text and its actions on the left, and one of four line
   drawings on the right, drawn as a hairline with a few construction lines, the way Blueprint draws everything. Each tone
   names three colours: --bnx-m draws the art (3:1 on the fill), --bnx-c writes the eyebrow (4.5:1) and --bnx-f is what
   the fill is a tint of. The fill is solid, so the close button and the art's own faces can wear it too. It has no border:
   the tint is its edge */
.bnx-slot{width:100%}
.bnx{position:relative;width:100%;min-height:176px;display:flex;flex-direction:column;justify-content:center;overflow:hidden;border-radius:var(--r-card);container:bnx/inline-size;isolation:isolate;
  --bnx-m:var(--accent-mark);--bnx-c:var(--accent-fg);--bnx-f:var(--accent);--bnx-k:10%;--bnx-ka:var(--bnx-k);
  --bnx-bg:color-mix(in srgb,var(--bnx-f) var(--bnx-ka),var(--surface));
  background:var(--bnx-bg);transition:background-color var(--t-slow) var(--ease)}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .bnx{--bnx-k:14%}}
:root[data-theme="dark"] .bnx{--bnx-k:14%}
.bnx[data-tone="neutral"]{--bnx-m:var(--muted);--bnx-c:var(--fg2);--bnx-bg:var(--chip)}
.bnx[data-tone="blue"]{--bnx-m:var(--pb);--bnx-c:var(--pb);--bnx-f:var(--pb);--bnx-ka:var(--bnx-k)}
.bnx[data-tone="violet"]{--bnx-m:var(--pl);--bnx-c:var(--pl);--bnx-f:var(--pl);--bnx-ka:var(--bnx-k)}
.bnx[data-tone="green"]{--bnx-m:var(--pm);--bnx-c:var(--pm);--bnx-f:var(--pm);--bnx-ka:var(--bnx-k)}
.bnx[data-tone="amber"]{--bnx-m:var(--py);--bnx-c:var(--py);--bnx-f:var(--py);--bnx-ka:var(--bnx-k)}
/* the text keeps clear of the art: its right padding is the art's width */
.bnx-tx{position:relative;z-index:1;display:flex;flex-direction:column;align-items:flex-start;padding:var(--s-5) calc(min(42%,300px) + var(--s-2)) var(--s-5) var(--s-6)}
.bnx-eb{font-size:12px;font-weight:500;line-height:16px;color:var(--bnx-c);transition:color var(--t-slow) var(--ease)}
.bnx-t{margin:var(--s-2) 0 0;font-size:14px;font-weight:500;line-height:20px;color:var(--fg);text-wrap:balance}
.bnx-d{margin:var(--s-1) 0 0;font-size:13px;line-height:20px;color:var(--fg2);text-wrap:pretty}
.bnx-acts{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-4);margin-top:var(--s-4)}
.bnx-lk{display:inline-flex;align-items:center;height:28px;font-size:13px;color:var(--fg);text-decoration:underline;text-decoration-color:color-mix(in srgb,currentColor 35%,transparent);text-underline-offset:3px;transition:text-decoration-color var(--t-fast)}
@media (hover:hover) and (pointer:fine){.bnx-lk:hover{text-decoration-color:currentColor}}
/* on a coloured banner the primary button is white in light mode, with the text colour on it, so it stands out from the tint
   in every tone; in dark mode, where the tint is deep, it stays the accent. A neutral (grey) banner keeps the accent */
.bnx:not([data-tone="neutral"]) .bnx-go{background:var(--surface);color:var(--fg);box-shadow:var(--shadow)}
@media (hover:hover) and (pointer:fine){.bnx:not([data-tone="neutral"]) .bnx-go:hover{background:var(--surface);box-shadow:var(--shadow-pop)}}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .bnx .bnx-go{background:var(--accent);color:var(--on-accent);box-shadow:none}
  @media (hover:hover) and (pointer:fine){:root:not([data-theme="light"]) .bnx .bnx-go:hover{background:var(--accent-2);box-shadow:none}}}
:root[data-theme="dark"] .bnx .bnx-go{background:var(--accent);color:var(--on-accent);box-shadow:none}
@media (hover:hover) and (pointer:fine){:root[data-theme="dark"] .bnx .bnx-go:hover{background:var(--accent-2);box-shadow:none}}
/* an optional close button in the top right corner (the card here leaves it out) */
.bnx-x{position:absolute;top:var(--s-2);right:var(--s-2);z-index:2;width:28px;height:28px;background:var(--bnx-bg)}
@media (hover:hover) and (pointer:fine){.bnx-x:hover{background:color-mix(in srgb,var(--fg) 8%,var(--bnx-bg))}}
/* the art: a 240 by 160 drawing at its own size in the middle of the area on the right, cropped by it and faded in from the
   text's side, so a narrower banner shows less of it rather than a smaller one */
.bnx-art{position:absolute;top:0;right:0;bottom:0;width:min(42%,300px);overflow:hidden;pointer-events:none;-webkit-mask-image:linear-gradient(90deg,transparent,#000 var(--s-8));mask-image:linear-gradient(90deg,transparent,#000 var(--s-8))}
.bnx-svg{position:absolute;left:50%;top:50%;width:240px;height:160px;margin:-80px 0 0 -120px;overflow:visible;display:block}
.bnx-svg .ln{fill:none;stroke:var(--bnx-m);stroke-width:1;stroke-linejoin:round;stroke-dasharray:1 1;stroke-dashoffset:0;animation:bnxdraw 1.1s var(--ease) calc(160ms + var(--d,0) * 60ms) backwards}
.bnx-svg .ac{stroke-width:1.5}
.bnx-svg .gd{fill:none;stroke:var(--bnx-m);stroke-width:1;stroke-dasharray:2 3;opacity:.45;animation:bnxfade .9s var(--ease) 60ms backwards}
/* the faces under the lines wear the fill, so what is in front hides what is behind once it is drawn */
.bnx-svg .pl{fill:var(--bnx-bg);stroke:none;animation:bnxfade .6s var(--ease) .5s backwards}
.bnx-svg .lg{fill:var(--bnx-m);animation:bnxfade .6s var(--ease) .7s backwards}
.bnx-svg .dt{fill:none;stroke:var(--bnx-m);stroke-width:2;stroke-linecap:round;stroke-dasharray:0 5;animation:bnxfade .9s var(--ease) .4s backwards}
@keyframes bnxdraw{from{stroke-dashoffset:1}}
@keyframes bnxfade{from{opacity:0}}
/* under the pointer the drawing pulls apart a little: each part moves by its depth (--z), with no blur */
.bnx-svg .z{transition:transform var(--t-slow) var(--ease)}
@media (hover:hover) and (pointer:fine){.bnx:hover .bnx-svg .z{transform:translate(calc(var(--z,0) * 1px),calc(var(--z,0) * -2.5px))}}
/* the card: the banner with a switcher under it that flips through the four styles, the two kept together and centred */
.dm.bnx-dm{min-height:0;gap:var(--s-5);justify-content:center}
.dm.bnx-dm>.dm-main{margin-block:0}
/* a phone, or any narrow column: the art becomes a shorter strip across the top that fades out before the text starts,
   so nothing is drawn behind the words. The drawing is shown at six tenths, so all of it fits in the strip, with its lines
   thickened to stay a hairline */
@container bnx (max-width:479px){
  .bnx-art{left:0;bottom:auto;width:auto;height:88px;-webkit-mask-image:linear-gradient(#000 68px,transparent 88px);mask-image:linear-gradient(#000 68px,transparent 88px)}
  .bnx-svg{top:46px;transform:scale(.6)}
  .bnx-svg :is(.ln,.gd){stroke-width:1.6}
  .bnx-svg .ac{stroke-width:2.4}
  .bnx-svg .dt{stroke-width:3.2}
  .bnx-tx{padding:84px var(--s-5) var(--s-5)}
}
@media (prefers-reduced-motion:reduce){.bnx-svg *{animation:none!important}.bnx-svg .z{transform:none!important}}
/* Blueprint: square, and the eyebrow as a small capital label */
:root:not([data-style="soft"]) .bnx:is(:not([data-tone]),[data-tone="accent"]):not(:where([data-style="soft"],[data-style="soft"] *)){--bnx-ka:20%}
@media (prefers-color-scheme:dark){:root:not([data-style="soft"]):not([data-theme="light"]) .bnx:is(:not([data-tone]),[data-tone="accent"]):not(:where([data-style="soft"],[data-style="soft"] *)){--bnx-ka:9%}}
:root:not([data-style="soft"])[data-theme="dark"] .bnx:is(:not([data-tone]),[data-tone="accent"]):not(:where([data-style="soft"],[data-style="soft"] *)){--bnx-ka:9%}
:root:not([data-style="soft"]) .bnx-eb:not(:where([data-style="soft"],[data-style="soft"] *)){font-size:11px;letter-spacing:.06em;text-transform:uppercase}
/* Blueprint draws its lines with square ends, so the dotted route is square dots */
:root:not([data-style="soft"]) .bnx-svg .dt:not(:where([data-style="soft"],[data-style="soft"] *)){stroke-dasharray:2 3}
@container bnx (max-width:479px){:root:not([data-style="soft"]) .bnx-svg .dt:not(:where([data-style="soft"],[data-style="soft"] *)){stroke-dasharray:3.2 4.8}}
banner.jsHTML · 63 lines
/* the four drawings, each 240 by 160 and written as plain paths, all in one isometric view on a floor grid, with their
   drawn box centred in it: layers (three plates of a stack, pulled apart, the bottom one standing on the floor), steps (a staircase climbing to a flag, on a floor
   that follows its footprint), tools (five tool tiles, Slack, GitHub, Linear, Gmail and Apollo, each with its mark on top)
   and path (a dotted route across the floor to a raised tile). .ln lines draw themselves in, in the order --d gives; .gd
   are the construction lines and the floor, dashed, which fade in; .pl are faces in the banner's fill that hide what is
   behind them; .lg are the tools' marks and .dt the dotted route; each .z group moves by its depth --z under the pointer. */
const BNX_ART = {
  layers:"<g class=\"gds\"><path class=\"gd\" d=\"M120 51L36.9 99M140.8 63L57.6 111M161.6 75L78.4 123M182.4 87L99.2 135M203.1 99L120 147M120 51L203.1 99M99.2 63L182.4 111M78.4 75L161.6 123M57.6 87L140.8 135M36.9 99L120 147\"/></g><g class=\"gds\"><path class=\"gd\" d=\"M78.4 43L78.4 93M161.6 43L161.6 93M120 67L120 117\"/></g><g class=\"z\" style=\"--z:0\"><path class=\"pl\" d=\"M120 69L161.6 93L161.6 99L120 123L78.4 99L78.4 93Z\"/><path class=\"ln\" style=\"--d:0\" pathLength=\"1\" d=\"M120 69L161.6 93L120 117L78.4 93Z\"/><path class=\"ln\" style=\"--d:1\" pathLength=\"1\" d=\"M161.6 93L161.6 99L120 123L78.4 99L78.4 93\"/><path class=\"ln\" style=\"--d:2\" pathLength=\"1\" d=\"M120 117L120 123\"/></g><g class=\"z\" style=\"--z:1\"><path class=\"pl\" d=\"M120 41L161.6 65L161.6 71L120 95L78.4 71L78.4 65Z\"/><path class=\"ln\" style=\"--d:3\" pathLength=\"1\" d=\"M120 41L161.6 65L120 89L78.4 65Z\"/><path class=\"ln\" style=\"--d:4\" pathLength=\"1\" d=\"M161.6 65L161.6 71L120 95L78.4 71L78.4 65\"/><path class=\"ln\" style=\"--d:5\" pathLength=\"1\" d=\"M120 89L120 95\"/></g><g class=\"z\" style=\"--z:2\"><path class=\"pl\" d=\"M120 13L161.6 37L161.6 43L120 67L78.4 43L78.4 37Z\"/><path class=\"ln\" style=\"--d:6\" pathLength=\"1\" d=\"M120 13L161.6 37L120 61L78.4 37Z\"/><path class=\"ln\" style=\"--d:7\" pathLength=\"1\" d=\"M161.6 37L161.6 43L120 67L78.4 43L78.4 37\"/><path class=\"ln\" style=\"--d:8\" pathLength=\"1\" d=\"M120 61L120 67\"/><path class=\"ln ac\" style=\"--d:9\" pathLength=\"1\" d=\"M120 23.8L142.9 37L120 50.2L97.1 37Z\"/></g>",
  steps:"<g class=\"gds\"><path class=\"gd\" d=\"M135.6 59L42.1 113M151.2 68L57.6 122M166.8 77L73.2 131M182.4 86L88.8 140M197.9 95L104.4 149M135.6 59L197.9 95M120 68L182.4 104M104.4 77L166.8 113M88.8 86L151.2 122M73.2 95L135.6 131M57.6 104L120 140M42.1 113L104.4 149\"/></g><g class=\"z\" style=\"--z:0\"><path class=\"pl\" d=\"M135.6 21L166.8 39L166.8 95L151.2 104L120 86L120 30Z\"/><path class=\"ln\" style=\"--d:0\" pathLength=\"1\" d=\"M135.6 21L166.8 39L151.2 48L120 30Z\"/><path class=\"ln\" style=\"--d:1\" pathLength=\"1\" d=\"M166.8 39L166.8 95L151.2 104L120 86L120 30\"/><path class=\"ln\" style=\"--d:2\" pathLength=\"1\" d=\"M151.2 48L151.2 104\"/><path class=\"pl\" d=\"M120 44L151.2 62L151.2 104L135.6 113L104.4 95L104.4 53Z\"/><path class=\"ln\" style=\"--d:3\" pathLength=\"1\" d=\"M120 44L151.2 62L135.6 71L104.4 53Z\"/><path class=\"ln\" style=\"--d:4\" pathLength=\"1\" d=\"M151.2 62L151.2 104L135.6 113L104.4 95L104.4 53\"/><path class=\"ln\" style=\"--d:5\" pathLength=\"1\" d=\"M135.6 71L135.6 113\"/><path class=\"pl\" d=\"M104.4 67L135.6 85L135.6 113L120 122L88.8 104L88.8 76Z\"/><path class=\"ln\" style=\"--d:6\" pathLength=\"1\" d=\"M104.4 67L135.6 85L120 94L88.8 76Z\"/><path class=\"ln\" style=\"--d:7\" pathLength=\"1\" d=\"M135.6 85L135.6 113L120 122L88.8 104L88.8 76\"/><path class=\"ln\" style=\"--d:8\" pathLength=\"1\" d=\"M120 94L120 122\"/><path class=\"pl\" d=\"M88.8 90L120 108L120 122L104.4 131L73.2 113L73.2 99Z\"/><path class=\"ln\" style=\"--d:9\" pathLength=\"1\" d=\"M88.8 90L120 108L104.4 117L73.2 99Z\"/><path class=\"ln\" style=\"--d:10\" pathLength=\"1\" d=\"M120 108L120 122L104.4 131L73.2 113L73.2 99\"/><path class=\"ln\" style=\"--d:11\" pathLength=\"1\" d=\"M104.4 117L104.4 131\"/></g><g class=\"z\" style=\"--z:2\"><path class=\"ln ac\" style=\"--d:12\" pathLength=\"1\" d=\"M137.1 30.9V8.9h14l-4 5 4 5h-14\"/></g>",
  tools:"<g class=\"gds\"><path class=\"gd\" d=\"M120 30.5L26.5 84.5M151.2 48.5L57.6 102.5M182.4 66.5L88.8 120.5M213.5 84.5L120 138.5M120 30.5L213.5 84.5M88.8 48.5L182.4 102.5M57.6 66.5L151.2 120.5M26.5 84.5L120 138.5\"/></g><g class=\"z\" style=\"--z:1\"><path class=\"pl\" d=\"M120 21.5L142.5 34.5L142.5 48.5L120 61.5L97.5 48.5L97.5 34.5Z\"/><path class=\"ln\" style=\"--d:0\" pathLength=\"1\" d=\"M120 21.5L142.5 34.5L120 47.5L97.5 34.5Z\"/><path class=\"ln\" style=\"--d:1\" pathLength=\"1\" d=\"M142.5 34.5L142.5 48.5L120 61.5L97.5 48.5L97.5 34.5\"/><path class=\"ln\" style=\"--d:2\" pathLength=\"1\" d=\"M120 47.5L120 61.5\"/><g class=\"lg\" transform=\"matrix(0.613 -0.354 0.613 0.354 105.278 34.5)\"><path d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z\"/></g><path class=\"pl\" d=\"M57.6 57.5L80.2 70.5L80.2 84.5L57.6 97.5L35.1 84.5L35.1 70.5Z\"/><path class=\"ln\" style=\"--d:3\" pathLength=\"1\" d=\"M57.6 57.5L80.2 70.5L57.6 83.5L35.1 70.5Z\"/><path class=\"ln\" style=\"--d:4\" pathLength=\"1\" d=\"M80.2 70.5L80.2 84.5L57.6 97.5L35.1 84.5L35.1 70.5\"/><path class=\"ln\" style=\"--d:5\" pathLength=\"1\" d=\"M57.6 83.5L57.6 97.5\"/><g class=\"lg\" transform=\"matrix(0.613 -0.354 0.613 0.354 42.924 70.5)\"><path d=\"M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z\"/></g><path class=\"pl\" d=\"M120 57.5L142.5 70.5L142.5 84.5L120 97.5L97.5 84.5L97.5 70.5Z\"/><path class=\"ln\" style=\"--d:6\" pathLength=\"1\" d=\"M120 57.5L142.5 70.5L120 83.5L97.5 70.5Z\"/><path class=\"ln\" style=\"--d:7\" pathLength=\"1\" d=\"M142.5 70.5L142.5 84.5L120 97.5L97.5 84.5L97.5 70.5\"/><path class=\"ln\" style=\"--d:8\" pathLength=\"1\" d=\"M120 83.5L120 97.5\"/><g class=\"lg\" transform=\"matrix(0.613 -0.354 0.613 0.354 105.278 70.5)\"><path d=\"M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z\"/></g><path class=\"pl\" d=\"M182.4 57.5L204.9 70.5L204.9 84.5L182.4 97.5L159.8 84.5L159.8 70.5Z\"/><path class=\"ln\" style=\"--d:9\" pathLength=\"1\" d=\"M182.4 57.5L204.9 70.5L182.4 83.5L159.8 70.5Z\"/><path class=\"ln\" style=\"--d:10\" pathLength=\"1\" d=\"M204.9 70.5L204.9 84.5L182.4 97.5L159.8 84.5L159.8 70.5\"/><path class=\"ln\" style=\"--d:11\" pathLength=\"1\" d=\"M182.4 83.5L182.4 97.5\"/><g class=\"lg\" transform=\"matrix(0.613 -0.354 0.613 0.354 167.631 70.5)\"><path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"/></g><path class=\"pl\" d=\"M120 93.5L142.5 106.5L142.5 120.5L120 133.5L97.5 120.5L97.5 106.5Z\"/><path class=\"ln\" style=\"--d:12\" pathLength=\"1\" d=\"M120 93.5L142.5 106.5L120 119.5L97.5 106.5Z\"/><path class=\"ln\" style=\"--d:13\" pathLength=\"1\" d=\"M142.5 106.5L142.5 120.5L120 133.5L97.5 120.5L97.5 106.5\"/><path class=\"ln\" style=\"--d:14\" pathLength=\"1\" d=\"M120 119.5L120 133.5\"/><g class=\"lg\" transform=\"matrix(0.613 -0.354 0.613 0.354 105.278 106.5)\"><g transform=\"scale(.663)\"><path d=\"M19.5993 0.0862365L19.605 13.2568C19.6058 15.3375 17.4222 16.6715 15.6079 15.6986L2.58376 8.7153C3.57706 7.05795 4.82616 5.57609 6.27427 4.32386L16.489 13.8945C17.0303 14.4015 17.8835 13.8518 17.6605 13.1398L13.6992 0.493553C15.0326 0.17147 16.4233 0 17.8536 0C18.4428 0 19.0248 0.0296814 19.5993 0.0862365Z\"/><path d=\"M16.0635 36.1087L16.0578 23.0046C16.057 20.9239 18.2407 19.5898 20.0549 20.5627L33.0838 27.5486C32.0838 29.2016 30.8289 30.6786 29.3751 31.925L19.1738 22.3668C18.6326 21.8598 17.7793 22.4095 18.0023 23.1215L21.9486 35.72C20.6338 36.0329 19.263 36.1989 17.8539 36.1989C17.2497 36.1989 16.6523 36.1683 16.0635 36.1087Z\"/><path d=\"M22.0105 16.77L31.4705 6.39392C30.2362 4.92008 28.7742 3.6486 27.1384 2.63702L20.2306 15.8767C19.2709 17.716 20.5871 19.9298 22.6396 19.9288L35.6183 19.923C35.6775 19.3234 35.7082 18.7151 35.7082 18.0996C35.7082 16.6683 35.5436 15.2761 35.2338 13.9406L22.7549 17.9576C22.0526 18.1837 21.5103 17.3187 22.0105 16.77Z\"/><path d=\"M0.0842758 16.3383L13.0237 16.3325C15.0764 16.3317 16.3923 18.5454 15.4327 20.3846L8.56047 33.5561C6.93095 32.547 5.47394 31.2801 4.24344 29.8121L13.653 19.4914C14.1531 18.9427 13.6107 18.0777 12.9084 18.3037L0.485078 22.3029C0.168551 20.954 0 19.5467 0 18.0994C0 17.5051 0.0290814 16.9177 0.0842758 16.3383Z\"/></g></g></g>",
  path:"<g class=\"gds\"><path class=\"gd\" d=\"M120 26L26.5 80M143.4 39.5L49.9 93.5M166.8 53L73.2 107M190.1 66.5L96.6 120.5M213.5 80L120 134M120 26L213.5 80M96.6 39.5L190.1 93.5M73.2 53L166.8 107M49.9 66.5L143.4 120.5M26.5 80L120 134\"/></g><g class=\"z\" style=\"--z:0\"><path class=\"dt\" d=\"M49.9 80L73.2 93.5L120 66.5L166.8 93.5L190.1 80\"/><path class=\"ln\" style=\"--d:0\" pathLength=\"1\" d=\"M49.9 74L60.2 80L49.9 86L39.5 80Z\"/></g><g class=\"z\" style=\"--z:2\"><path class=\"pl\" d=\"M190.1 51L205.7 60L205.7 80L190.1 89L174.6 80L174.6 60Z\"/><path class=\"ln ac\" style=\"--d:1\" pathLength=\"1\" d=\"M190.1 51L205.7 60L190.1 69L174.6 60Z\"/><path class=\"ln\" style=\"--d:2\" pathLength=\"1\" d=\"M205.7 60L205.7 80L190.1 89L174.6 80L174.6 60\"/><path class=\"ln\" style=\"--d:3\" pathLength=\"1\" d=\"M190.1 69L190.1 89\"/></g>"
};
DM.def('banner', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{x}', I('x'));
  const root = $('.bnx', r), art = $('.bnx-art', root), xB = $('.bnx-x', root);
  const T = { eb:$('.bnx-eb>span', root), t:$('.bnx-t>span', root), d:$('.bnx-d>span', root), a:$('.bnx-go>span', root), s:$('.bnx-lk>span', root) };
  // the drawing named by data-art; drawing it again plays its entrance again
  const draw = () => { const k = root.dataset.art; art.innerHTML = BNX_ART[k] ? `<svg class="bnx-svg" viewBox="0 0 240 160" width="240" height="160" focusable="false">${BNX_ART[k]}</svg>` : ''; };
  draw();
  // an optional close button (.bnx-x inside the banner; the card here leaves it out): it first sends a cancelable
  // banner-dismiss event from the banner, so your code can keep it, or remember it was closed, then the banner plays out
  // and is hidden. Where the focus goes next is up to your page
  if (xB) xB.onclick = () => {
    if (root.hidden || root.classList.contains('leave')) return;
    if (!root.dispatchEvent(new CustomEvent('banner-dismiss', { bubbles:true, cancelable:true }))) return;
    root.classList.add('leave'); c.later(REDUCE ? 0 : 320, () => { root.classList.remove('leave'); root.hidden = true; });
  };

  // the preview's four styles, one per tone and drawing: the tone, the drawing and the words change in place
  const P = [
    { tone:'accent', art:'layers', eb:'New', t:'Memory that carries across threads', d:'Agents keep what they learn about your project, so you only explain things once.', a:'Turn on memory', s:'What it keeps' },
    { tone:'violet', art:'tools', eb:'Beta', t:'Connect your tools in one place', d:'Linear, GitHub and Slack in one list, with access you can scope for each agent.', a:'Connect a tool', s:'Read the docs' },
    { tone:'green', art:'steps', eb:'Guide', t:'Set up your first automation', d:'Five short steps, about ten minutes. The agent can do most of them for you.', a:'Start the guide', s:'Maybe later' },
    { tone:'amber', art:'path', eb:'Heads-up', t:'Maintenance on Sunday, 02:00 UTC', d:'Agents pause for about an hour, and queued runs pick up where they left off.', a:'Add to calendar', s:'Status page' }
  ];
  const show = p => { root.dataset.tone = p.tone; root.dataset.art = p.art; for (const k in T) if (T[k]) swap(T[k], p[k]); draw(); };
  const sg = $('.demo .seg2', r);
  if (!sg) return;
  // the banner keeps one height, the tallest of the four at this width, so flipping styles never moves the switcher under it
  // or the banner's edges. It is measured again when the width changes
  const fit = () => {
    if (!root.isConnected || !root.offsetWidth) return;
    const was = {}; for (const k in T) if (T[k]) was[k] = T[k].textContent;
    root.style.minHeight = ''; let h = 0;
    P.forEach(p => { for (const k in T) if (T[k]) T[k].textContent = p[k]; h = Math.max(h, root.offsetHeight); });
    for (const k in T) if (T[k]) T[k].textContent = was[k];
    root.style.minHeight = Math.ceil(h) + 'px';
  };
  let wide = 0;
  const ro = new ResizeObserver(() => { const w = root.offsetWidth; if (w !== wide){ wide = w; fit(); } }); ro.observe(root); c.offs.push(() => ro.disconnect());
  if (document.fonts) document.fonts.ready.then(() => { if (!c.dead) fit(); });
  // the switcher under it (the card's tabs on designme.dev's homepage): picking a style is the person taking over, so the
  // demo stops there
  let at = 0;
  const knob = seg(sg, v => { c.mine = true; if (+v !== at){ at = +v; show(P[at]); } });
  const pick = i => { at = i; $$('button', sg).forEach((b, j) => b.setAttribute('aria-pressed', j === i)); knob.place(); show(P[i]); };
  // the demo flips through the four styles twice, a few seconds each, and rests on the first. A mouse resting on it holds
  // it where it is; a click, a key or the focus of yours stops it for good. With reduced motion it doesn't play
  if (r === c.st && c.card && !REDUCE) c.run(async () => {
    for (let k = 1; k <= P.length * 2; k++){ await c.step(4000); pick(k % P.length); }
  });
});
06

Questions

What people ask about it.

Can I use my own illustration?
Yes. Pass an SVG element to art. It is shown at 240 by 160 in the same area and cropped the same way. Give its lines className="dm-ln" and pathLength={1} and they draw themselves in like the built-in ones.
How do I keep a banner closed across visits?
Store it yourself. In HTML, listen for banner-dismiss and save that it was closed; the next time, leave the banner out of the page.
Why is the primary button white on a coloured banner?
In light mode an accent button on a tinted banner blends into the tint, and on the accent's own tint it nearly disappears. A white button with the text colour on it stands out on every tone and keeps the label at full contrast. In dark mode the tint is deep enough for the accent to stand out, so it stays the accent.
What happens on a phone?
Below 480px wide the drawing moves to a short strip across the top, shown at six tenths of its size so all of it fits, and the text runs the full width under it.
07

More in Collaboration

Threads, inboxes and boards for teams working alongside an agent.

CommentsA design thread where you can @mention the agent and it replies.
NotificationsAn inbox where agent updates and mentions arrive live.
Invite to collaborateAdd teammates by email as chips, set their role, send.
ReactionsQuick reactions on an agent update, with counts that pop.
Profile menuThe account menu behind an avatar: switch workspace, theme, shortcuts and sign out.
Context switcherThe organisation, project and environment you are in, each part a searchable switcher.
Filter menuAdd filters from a menu, see them as chips, and watch the count change.
Ticket listSupport tickets the agent has triaged, with suggested replies.
All 10 in Collaboration →