Skip to content

Slide-over cart whose lines collapse away and whose totals roll as you edit.

Commerce & billing@base-ui/react@number-flow/reactmotion

01Preview

Atelier Nord

Linen overshirt

€89

Washed Belgian linen with a relaxed shoulder and horn buttons. Sand, size M.

02Install

Copy the source into your project. It becomes yours: no package to update, no wrapper between you and the markup. It needs:

npm install @base-ui/react @number-flow/react motion

03Usage

import {
  CartDrawer, CartDrawerTrigger, CartDrawerContent, CartDrawerClose,
  CartLines, CartLine, CartSummary, CartShippingProgress, CartEmpty,
} from "@/components/ui/cart-drawer";

<CartDrawer currency="EUR" locale="en-IE">
  <CartDrawerTrigger>Cart</CartDrawerTrigger>
  <CartDrawerContent count={cart.count}>
    {cart.lines.length ? (
      <>
        <CartLines>
          {cart.lines.map((line) => (
            <CartLine
              key={line.id}
              name={line.name}
              variant={line.variant}
              price={line.price}
              image={line.image}
              quantity={line.quantity}
              max={line.stock}
              onQuantityChange={(q) => cart.update(line.id, q)}
              onRemove={() => cart.remove(line.id)}
            />
          ))}
        </CartLines>
        <CartSummary subtotal={cart.subtotal} onCheckout={() => router.push("/checkout")}>
          <CartShippingProgress subtotal={cart.subtotal} threshold={200} />
        </CartSummary>
      </>
    ) : (
      <CartEmpty action={<CartDrawerClose>Continue shopping</CartDrawerClose>} />
    )}
  </CartDrawerContent>
</CartDrawer>

04Source

"use client";
import { Drawer } from "@base-ui/react/drawer";
import NumberFlow from "@number-flow/react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { createContext, useContext, useEffect, useRef, useState } from "react";
import { QuantityStepper } from "@/components/ui/quantity-stepper";
import { cn } from "@/lib/cn";
import { ArrowRight, Cart, Check, Image as ImageIcon, Loader, X } from "@/lib/icons";
import { ease } from "@/lib/motion";

type Money = { currency: string; locale: string };
const MoneyContext = createContext<Money>({ currency: "USD", locale: "en-US" });

/** Formats an amount in the drawer's currency. Whole amounts drop the ".00". */
export function useMoney() {
  const { currency, locale } = useContext(MoneyContext);
  return (amount: number) =>
    new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: Number.isInteger(amount) ? 0 : 2 }).format(amount);
}

function moneyFormat(amount: number, currency: string) {
  return { style: "currency", currency, minimumFractionDigits: Number.isInteger(amount) ? 0 : 2, maximumFractionDigits: 2 } as const;
}

export type CartDrawerProps = Omit<Drawer.Root.Props, "swipeDirection" | "snapPoints" | "snapPoint" | "defaultSnapPoint" | "onSnapPointChange"> & {
  /** ISO currency code for every amount inside. */
  currency?: string;
  /** Locale for number formatting. Set it explicitly so server and client agree. */
  locale?: string;
};

/** The cart as a slide-over from the right edge. Swipe right to dismiss on touch. */
export function CartDrawer({ currency = "USD", locale = "en-US", ...rest }: CartDrawerProps) {
  return (
    <MoneyContext.Provider value={{ currency, locale }}>
      <Drawer.Root swipeDirection="right" {...rest} />
    </MoneyContext.Provider>
  );
}

export type CartDrawerTriggerProps = Drawer.Trigger.Props;
export function CartDrawerTrigger(props: CartDrawerTriggerProps) {
  return <Drawer.Trigger {...props} />;
}

export type CartDrawerCloseProps = Omit<Drawer.Close.Props, "className"> & { className?: string };
/** A secondary button that closes the drawer, e.g. "Continue shopping". */
export function CartDrawerClose({ className, ...rest }: CartDrawerCloseProps) {
  return (
    <Drawer.Close
      className={cn(
        "inline-flex h-8 select-none items-center justify-center gap-2 rounded-lg border border-line-2 bg-raised px-3 text-[12.5px] font-medium text-fg shadow-[var(--shadow)] outline-none",
        "transition-[background-color,border-color,scale] duration-150 hover:border-fg-4 hover:bg-hover active:scale-[0.97] active:duration-75",
        "focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3",
        className,
      )}
      {...rest}
    />
  );
}

export type CartDrawerContentProps = Omit<Drawer.Popup.Props, "className" | "style" | "title"> & {
  className?: string;
  style?: React.CSSProperties;
  /** Render into this element instead of document.body. Backdrop and panel become absolute to it. */
  container?: Drawer.Portal.Props["container"];
  title?: React.ReactNode;
  /** Units in the cart, shown and rolled beside the title. */
  count?: number;
};

export function CartDrawerContent({ container, title = "Cart", count, className, style, children, ...rest }: CartDrawerContentProps) {
  const contained = container != null;
  return (
    <Drawer.Portal container={container}>
      <Drawer.Backdrop
        className={cn(
          contained ? "absolute" : "fixed",
          "inset-0 z-(--z-overlay) bg-overlay",
          "opacity-[calc(1-var(--drawer-swipe-progress,0))] transition-opacity duration-300 ease-drawer",
          "data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:duration-200 data-swiping:duration-0",
        )}
      />
      <Drawer.Viewport className={cn(contained ? "absolute" : "fixed", "inset-0 z-(--z-dialog) flex justify-end p-2")}>
        <Drawer.Popup
          style={style}
          className={cn(
            "relative flex h-full w-[400px] max-w-full flex-col overflow-hidden rounded-2xl border border-line-2 bg-raised text-fg shadow-pop outline-none",
            // Slides in on the drawer curve, follows the finger 1:1, and a flick shortens the exit.
            "[transform:translateX(var(--drawer-swipe-movement-x,0px))] transition-transform duration-[340ms] ease-drawer data-swiping:duration-0 data-swiping:select-none",
            "data-starting-style:[transform:translateX(calc(100%+8px))] data-ending-style:[transform:translateX(calc(100%+8px))]",
            "data-ending-style:duration-[calc(var(--drawer-swipe-strength,1)*240ms)]",
            className,
          )}
          {...rest}
        >
          <div className="flex h-13 shrink-0 items-center gap-2 border-b border-line pl-4 pr-2.5">
            <Drawer.Title className="text-[14px] font-medium tracking-[-0.015em] text-fg">{title}</Drawer.Title>
            {count != null && count > 0 && (
              <span className="tabular grid h-5 min-w-5 place-items-center rounded-full bg-hover px-1.5 text-[11px] font-medium text-fg-2">
                <NumberFlow value={count} className="leading-none" />
                <span className="sr-only">{count === 1 ? " item" : " items"}</span>
              </span>
            )}
            <Drawer.Close
              aria-label="Close cart"
              className={cn(
                "relative ml-auto grid size-8 place-items-center rounded-lg text-fg-3 outline-none",
                "transition-[background-color,color,scale] duration-150 hover:bg-hover hover:text-fg active:scale-[0.92] active:duration-75",
                "focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3",
                "before:absolute before:-inset-1.5 before:content-[''] pointer-fine:before:hidden",
              )}
            >
              <X />
            </Drawer.Close>
          </div>
          <Drawer.Content className="flex min-h-0 flex-1 flex-col">{children}</Drawer.Content>
        </Drawer.Popup>
      </Drawer.Viewport>
    </Drawer.Portal>
  );
}

export type CartLinesProps = React.ComponentProps<"ul">;

/** The scrolling list of lines. Hairlines appear at the edges only when there is more to scroll. */
export function CartLines({ className, children, ...rest }: CartLinesProps) {
  const ref = useRef<HTMLUListElement>(null);
  const [edges, setEdges] = useState({ top: false, bottom: false });
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const read = () => {
      const top = el.scrollTop > 1;
      const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1;
      setEdges((e) => (e.top === top && e.bottom === bottom ? e : { top, bottom }));
    };
    // Lines grow and collapse without scrolling, so watch their sizes too.
    const ro = new ResizeObserver(read);
    const watch = () => {
      ro.disconnect();
      ro.observe(el);
      for (const c of el.children) ro.observe(c);
      read();
    };
    const mo = new MutationObserver(watch);
    mo.observe(el, { childList: true });
    watch();
    el.addEventListener("scroll", read, { passive: true });
    return () => {
      el.removeEventListener("scroll", read);
      mo.disconnect();
      ro.disconnect();
    };
  }, []);

  return (
    <div className="relative flex min-h-0 flex-1 flex-col">
      <span aria-hidden className={cn("pointer-events-none absolute inset-x-0 top-0 z-1 h-px bg-line transition-opacity duration-150", edges.top ? "opacity-100" : "opacity-0")} />
      <ul
        ref={ref}
        aria-label="Items in your cart"
        className={cn("min-h-0 flex-1 overflow-y-auto overscroll-contain px-2 pb-8 pt-1.5", className)}
        {...rest}
      >
        <AnimatePresence initial={false}>{children}</AnimatePresence>
      </ul>
      <span aria-hidden className={cn("pointer-events-none absolute inset-x-0 bottom-0 z-1 h-px bg-line transition-opacity duration-150", edges.bottom ? "opacity-100" : "opacity-0")} />
    </div>
  );
}

export type CartLineProps = Omit<React.ComponentProps<"li">, "onChange"> & {
  name: string;
  /** Size, colour and other options, as one short line. */
  variant?: string;
  /** Unit price. The line shows the total and, above 1, the unit price. */
  price: number;
  quantity: number;
  onQuantityChange: (quantity: number) => void;
  onRemove: () => void;
  /** An image URL, or any node (a thumbnail component). */
  image?: string | React.ReactNode;
  /** Units available. */
  max?: number;
  /** Price before a sale, struck through beside the total. */
  compareAt?: number;
};

export function CartLine({ name, variant, price, quantity, onQuantityChange, onRemove, image, max, compareAt, className, ...rest }: CartLineProps) {
  const { currency, locale } = useContext(MoneyContext);
  const money = useMoney();
  const reduce = useReducedMotion();
  const [moving, setMoving] = useState(false);
  const total = price * quantity;

  return (
    <motion.li
      // Collapse on remove: the rows below close the gap instead of jumping.
      initial={reduce ? { opacity: 0 } : { opacity: 0, height: 0 }}
      animate={{ opacity: 1, height: "auto" }}
      exit={reduce ? { opacity: 0 } : { opacity: 0, height: 0, filter: "blur(2px)" }}
      transition={{ duration: 0.26, ease: ease.inOut }}
      // Clipped only while the height moves, so the stock hint can overhang the row at rest.
      onAnimationStart={() => setMoving(true)}
      onAnimationComplete={() => setMoving(false)}
      className={cn("relative", moving && "overflow-hidden", className)}
      {...(rest as React.ComponentProps<typeof motion.li>)}
    >
      <div className="flex gap-3 rounded-xl px-2 py-2.5">
        <Thumb image={image} name={name} />
        <div className="flex min-w-0 flex-1 flex-col gap-2">
          <div className="flex items-start justify-between gap-3">
            <div className="min-w-0">
              <p className="text-[13px] font-medium leading-[1.35] text-fg">{name}</p>
              {variant && <p className="truncate text-[12px] text-fg-3">{variant}</p>}
            </div>
            <div className="flex shrink-0 flex-col items-end">
              {/* Fixed height: the rolling digits carry their own mask padding, which must not move the row. */}
              <span className="flex h-[18px] items-center">
                <NumberFlow value={total} locales={locale} format={moneyFormat(total, currency)} className="tabular text-[13px] font-medium leading-none text-fg" />
              </span>
              {/* Always one line tall, so the stepper row never moves when "each" appears. */}
              <span className="tabular h-4 text-[11.5px] leading-4 text-fg-4">
                {compareAt != null && compareAt > price ? (
                  <s className="decoration-fg-4">{money(compareAt * quantity)}</s>
                ) : quantity > 1 ? (
                  `${money(price)} each`
                ) : null}
              </span>
            </div>
          </div>
          <div className="flex items-center justify-between gap-3">
            <QuantityStepper
              size="sm"
              value={quantity}
              onValueChange={onQuantityChange}
              max={max}
              label={`Quantity of ${name}`}
              onRemove={onRemove}
              removeLabel={`Remove ${name}`}
              hintSide="bottom"
            />
            <button
              type="button"
              onClick={onRemove}
              aria-label={`Remove ${name}`}
              className={cn(
                "relative rounded-sm text-[12px] text-fg-3 outline-none transition-colors duration-150 hover:text-fg",
                "focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3",
                "before:absolute before:-inset-x-2 before:-inset-y-3 before:content-[''] pointer-fine:before:hidden",
              )}
            >
              Remove
            </button>
          </div>
        </div>
      </div>
    </motion.li>
  );
}

function Thumb({ image, name }: { image: CartLineProps["image"]; name: string }) {
  const [failed, setFailed] = useState(false);
  const frame = "relative grid size-16 shrink-0 place-items-center overflow-hidden rounded-lg border border-line bg-hover text-fg-4";
  if (typeof image === "string" && !failed)
    return (
      <span className={frame}>
        {/* eslint-disable-next-line @next/next/no-img-element -- a copied component can't assume next/image */}
        <img src={image} alt={name} width={64} height={64} loading="lazy" onError={() => setFailed(true)} className="size-full object-cover" />
      </span>
    );
  return (
    <span aria-hidden className={frame}>
      {typeof image === "string" || image == null ? <ImageIcon size={18} /> : image}
    </span>
  );
}

export type CartShippingProgressProps = React.ComponentProps<"div"> & {
  subtotal: number;
  /** Subtotal at which shipping becomes free. */
  threshold: number;
};

/** "€23 away from free shipping", with a bar that fills as the cart grows. */
export function CartShippingProgress({ subtotal, threshold, className, ...rest }: CartShippingProgressProps) {
  const { currency, locale } = useContext(MoneyContext);
  const reduce = useReducedMotion();
  const left = Math.max(0, threshold - subtotal);
  const done = left === 0;
  const progress = Math.min(1, subtotal / threshold);

  return (
    <div className={cn("flex flex-col gap-2", className)} {...rest}>
      <p className="flex h-4 items-center gap-1.5 text-[12px] text-fg-2" aria-live="polite">
        <AnimatePresence initial={false} mode="popLayout">
          {done ? (
            <motion.span
              key="done"
              className="flex items-center gap-1.5 text-fg"
              initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6, filter: "blur(2px)" }}
              animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
              exit={{ opacity: 0, transition: { duration: 0.1 } }}
              transition={{ duration: 0.24, ease: ease.out }}
            >
              <Check size={14} className="text-success" />
              Free shipping unlocked
            </motion.span>
          ) : (
            <motion.span
              key="left"
              className="flex items-center"
              initial={reduce ? { opacity: 0 } : { opacity: 0, y: -6, filter: "blur(2px)" }}
              animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
              exit={{ opacity: 0, transition: { duration: 0.1 } }}
              transition={{ duration: 0.24, ease: ease.out }}
            >
              <NumberFlow value={left} locales={locale} format={moneyFormat(left, currency)} className="tabular font-medium text-fg" />
              <span>&nbsp;away from free shipping</span>
            </motion.span>
          )}
        </AnimatePresence>
      </p>
      <div
        role="progressbar"
        aria-label="Progress to free shipping"
        aria-valuemin={0}
        aria-valuemax={100}
        aria-valuenow={Math.round(progress * 100)}
        className="h-1 overflow-hidden rounded-full bg-hover"
      >
        <div
          className={cn(
            "h-full origin-left rounded-full transition-[transform,background-color] duration-500 ease-out-expo",
            done ? "bg-success" : "bg-fg",
          )}
          style={{ transform: `scaleX(${progress})` }}
        />
      </div>
    </div>
  );
}

export type CartSummaryProps = Omit<React.ComponentProps<"div">, "children"> & {
  subtotal: number;
  /** One quiet line under the subtotal. */
  note?: React.ReactNode;
  /** Runs on Check out. Return a promise to show the busy state on the button. */
  onCheckout?: () => void | Promise<unknown>;
  checkoutLabel?: string;
  /** Anything above the totals, like CartShippingProgress. */
  children?: React.ReactNode;
};

export function CartSummary({ subtotal, note = "Shipping and taxes are calculated at checkout.", onCheckout, checkoutLabel = "Check out", className, children, ...rest }: CartSummaryProps) {
  const { currency, locale } = useContext(MoneyContext);
  const [busy, setBusy] = useState(false);
  const pending = useRef(false);
  const timer = useRef<number>(undefined);
  useEffect(() => () => window.clearTimeout(timer.current), []);

  return (
    <div className={cn("flex shrink-0 flex-col gap-3 border-t border-line px-4 pb-4 pt-3.5", className)} {...rest}>
      {children}
      <div className="flex h-5 items-center justify-between">
        <span className="text-[13px] text-fg-2">Subtotal</span>
        <NumberFlow value={subtotal} locales={locale} format={moneyFormat(subtotal, currency)} className="tabular text-[15px] font-medium leading-none tracking-[-0.01em] text-fg" />
      </div>
      {note && <p className="-mt-2 text-[12px] text-fg-3">{note}</p>}
      <button
        type="button"
        aria-busy={busy || undefined}
        onClick={async () => {
          // One checkout at a time, even before the spinner shows.
          if (pending.current) return;
          const result = onCheckout?.();
          if (!(result instanceof Promise)) return;
          pending.current = true;
          const shownAt = { t: 0 };
          timer.current = window.setTimeout(() => {
            shownAt.t = performance.now();
            setBusy(true);
          }, 150);
          try {
            await result;
          } finally {
            window.clearTimeout(timer.current);
            const hold = shownAt.t ? Math.max(0, 300 - (performance.now() - shownAt.t)) : 0;
            timer.current = window.setTimeout(() => {
              pending.current = false;
              setBusy(false);
            }, hold);
          }
        }}
        className={cn(
          "group/checkout relative inline-flex h-9 w-full select-none items-center justify-center rounded-lg bg-fg text-[13px] font-medium text-frame outline-none",
          "transition-[background-color,scale] duration-150 hover:bg-fg/90 active:scale-[0.98] active:duration-75 aria-busy:cursor-progress aria-busy:active:scale-100",
          "focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3",
        )}
      >
        <span className={cn("flex items-center gap-2 transition-opacity duration-150", busy && "opacity-0")}>
          {checkoutLabel}
          <ArrowRight size={15} className="transition-transform duration-200 ease-out-expo group-hover/checkout:translate-x-0.5" />
        </span>
        {busy && (
          <span className="absolute inset-0 grid place-items-center">
            <Loader size={16} className="animate-spin motion-reduce:animate-none" />
            <span className="sr-only">Starting checkout</span>
          </span>
        )}
      </button>
    </div>
  );
}

export type CartEmptyProps = React.ComponentProps<"div"> & {
  title?: React.ReactNode;
  description?: React.ReactNode;
  /** One action, usually a CartDrawerClose that says "Continue shopping". */
  action?: React.ReactNode;
};

export function CartEmpty({ title = "Your cart is empty", description, action, className, ...rest }: CartEmptyProps) {
  const reduce = useReducedMotion();
  return (
    <motion.div
      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 6 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.28, ease: ease.out, delay: 0.12 }}
      className={cn("flex flex-1 flex-col items-center justify-center gap-3 px-6 pb-10 text-center", className)}
      {...(rest as React.ComponentProps<typeof motion.div>)}
    >
      <span aria-hidden className="grid size-10 place-items-center rounded-full border border-line bg-hover text-fg-3">
        <Cart />
      </span>
      <div className="flex flex-col gap-1">
        <p className="text-[13px] font-medium text-fg">{title}</p>
        {description && <p className="max-w-[28ch] text-balance text-[12.5px] text-fg-3">{description}</p>}
      </div>
      {action && <div className="pt-1">{action}</div>}
    </motion.div>
  );
}

05Props

CartDrawer

PropTypeDefaultDescription
currencystring"USD"ISO code used by every amount inside.
localestring"en-US"Number formatting locale. Set it so server and client agree.
openbooleanControlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean, details) => voidCalled when it opens or closes, including Escape, outside press and swipe.
modalboolean | "trap-focus"truetrue locks page scroll and traps focus; "trap-focus" only traps focus.

CartDrawerContent

PropTypeDefaultDescription
countnumberUnits in the cart, rolled beside the title. Hidden at 0.
titleReactNode"Cart"The drawer's heading and accessible name.
containerHTMLElement | RefObjectPortal target. When set, the backdrop and panel position inside it instead of the viewport.

CartLine

PropTypeDefaultDescription
name*stringThe product name. Wraps rather than truncating.
variantstringOptions in one line, e.g. "Sand · M". Truncates.
price*numberUnit price. The line shows price × quantity, and the unit price above 1.
compareAtnumberPre-sale unit price, struck through under the total.
quantity*numberUnits of this line.
onQuantityChange*(quantity: number) => voidCalled with each new quantity.
onRemove*() => voidCalled from the Remove link and from the stepper's trash at 1.
maxnumberUnits in stock. The stepper stops and explains.
imagestring | ReactNodeImage URL (64×64, lazy, falls back to a neutral tile on error) or any node.

CartSummary

PropTypeDefaultDescription
subtotal*numberRolled to each new value.
noteReactNode"Shipping and taxes are calculated at checkout."One quiet line under the subtotal.
onCheckout() => void | Promise<unknown>A promise shows a spinner over the label after 150ms and blocks repeat presses.
checkoutLabelstring"Check out"The primary button's label.
childrenReactNodeRendered above the totals, e.g. CartShippingProgress or an undo line.

CartShippingProgress

PropTypeDefaultDescription
subtotal*numberThe current subtotal.
threshold*numberSubtotal at which shipping is free.

CartEmpty

PropTypeDefaultDescription
titleReactNode"Your cart is empty"One line saying what would be here.
descriptionReactNodeOptional second line.
actionReactNodeOne action, usually CartDrawerClose with "Continue shopping".

CartLines

PropTypeDefaultDescription
childrenCartLine[]Keyed lines. Removing one collapses it while the rest close the gap.

CartDrawerTrigger

PropTypeDefaultDescription
renderReactElementPut the drawer's behaviour on your own button, like a cart icon with a count.

CartDrawerClose

PropTypeDefaultDescription
childrenReactNodeA secondary button that closes the drawer.

useMoney

PropTypeDefaultDescription
(amount: number) => stringhookFormats an amount in the drawer's currency and locale; whole amounts drop the .00.

06Notes

Behavior

  • Every total (line, subtotal, amount left for free shipping) rolls to its new value inside a fixed-height box, so nothing in the row moves as digits change.
  • The unit price line is always reserved, so the stepper row stays put when "€89 each" appears at a quantity of 2.
  • The list scrolls on its own with the summary pinned; hairlines appear at the top and bottom edges only when there is more to scroll.
  • Checkout guards against double presses from the first click, before the spinner even appears, and holds the spinner at least 300ms once shown.

Motion

  • The panel slides from the right on the drawer curve in 340ms and follows the finger 1:1 when swiped; a flick shortens the exit to 240ms × swipe strength.
  • A removed line collapses its height and fades with a 2px blur over 260ms on the in-out curve; the empty state rises 6px after a 120ms beat.
  • The free-shipping bar grows with a 500ms expo ease-out and turns success green, and its message swaps with a 6px slide.
  • Reduced motion keeps the fades and drops collapse, slide and blur.

Accessibility

  • A Base UI drawer: the title names the dialog, focus moves in on open, is trapped, and returns to the trigger on Escape or close.
  • Each line's controls are named after the product ("Quantity of Linen overshirt", "Remove Linen overshirt"), and the list is labelled "Items in your cart".
  • The shipping bar is a progressbar with a value, and its message is a polite live region.