sdxc

Type to search, or start from one of these:

Drawer

A preset of Dialog docked flush against one physical edge of the viewport, sized to fill that edge and sliding into place from it on open, back out on close.

Your cart

Shipping is on us on this order.

Felt desk mat

Charcoal · 90 × 40 cm

$45.00
Low-profile keycaps

Warm grey · ISO

$138.00
Coiled USB-C cable

Aviator · 1.5 m

$32.00
Subtotal$215.00
ShippingFree
Total$215.00
View code
let lines: CartLine[] = CART_LINES.map((line) => ({ ...line }));

function changeQuantity(event: Event & { currentTarget: HTMLInputElement }) {
	let line = lines.find((candidate) => candidate.id === event.currentTarget.dataset.line);
	if (!line) return;
	line.quantity = Math.max(1, Number(event.currentTarget.value) || 1);
	void handle.update();
}

let totals = cartTotals(lines);

<Button commandfor="cart" command="show-modal">
	<ShoppingCartIcon />
	Cart · {totals.itemCount}
</Button>

<Drawer id="cart" placement="right" aria-labelledby="cart-title">
	<Drawer.Header>
		<Drawer.Title id="cart-title">Your cart</Drawer.Title>
		<Drawer.Description>
			{totals.freeShipping ? "Shipping is on us on this order." : "Spend $150 to get free shipping."}
		</Drawer.Description>
	</Drawer.Header>

	<div
		mix={[
			vstack({ gap: 4, align: "stretch" }),
			// The dock is as tall as the viewport, so everything between the header and the
			// footer scrolls as one column — the checkout button never slides out of reach.
			grow(),
			minBs(0),
			overflowY("auto"),
		]}
	>
		{lines.map((line) => (
			<Item key={line.id}>
				<Item.Content>
					<Item.Title>{line.name}</Item.Title>
					<Item.Description>{line.variant}</Item.Description>
					<div mix={[hstack({ gap: 2, align: "center", justify: "between" }), mbs(1)]}>
						<NumberField.Group mix={[stepper()]}>
							<NumberField.DecrementButton
								command={NUMBER_FIELD_STEP_DOWN_COMMAND}
								commandfor={`cart-quantity-${line.id}`}
								aria-label={`One fewer ${line.name}`}
							/>
							<NumberField.Input
								id={`cart-quantity-${line.id}`}
								name={`quantity[${line.id}]`}
								data-line={line.id}
								min={1}
								max={99}
								value={String(line.quantity)}
								aria-label={`Quantity of ${line.name}`}
								mix={[on<HTMLInputElement, "input">("input", changeQuantity)]}
							/>
							<NumberField.IncrementButton
								command={NUMBER_FIELD_STEP_UP_COMMAND}
								commandfor={`cart-quantity-${line.id}`}
								aria-label={`One more ${line.name}`}
							/>
						</NumberField.Group>
						<span>{formatPrice(line.unitPrice * line.quantity)}</span>
					</div>
				</Item.Content>
			</Item>
		))}

		<Separator />

		<Text>Subtotal</Text>
		<Text>{totals.subtotal}</Text>
		<Text>Shipping</Text>
		<Text>{totals.shipping}</Text>
		<Text>Total</Text>
		<Text>{totals.total}</Text>
	</div>

	<Drawer.Footer>
		<Button commandfor="cart" command="close" variant="outline" color="neutral">
			Keep shopping
		</Button>
		<Button commandfor="cart" command="close">Checkout · {totals.total}</Button>
	</Drawer.Footer>
	<Drawer.Close commandfor="cart" aria-label="Close the cart" />
</Drawer>

Installation

An ordinary dependency: install it, import the theme once, and import the component where it is used.

npm add @sdxc/ui
import "@sdxc/ui/theme.css";

Usage

import { Drawer } from "@sdxc/ui";

Renders Dialog pinned to one edge of the viewport, animating a transform off the native open attribute so @starting-style can animate the close; a mix prop composes alongside this placement styling.

Examples

<Drawer id="filters" placement="bottom" aria-labelledby="filters-title">
	<Drawer.Header>
		<Drawer.Title id="filters-title">{t("filters.title")}</Drawer.Title>
	</Drawer.Header>
	<Drawer.Footer>
		<Button commandfor="filters" command="close">{t("actions.apply")}</Button>
	</Drawer.Footer>
</Drawer>

Props

Read from the component's own types, so every prop, every default and every allowed value is listed.

PropTypeDescription
placement?"top" | "right" | "bottom" | "left"Physical edge of the viewport to dock against and slide in from. Defaults to DEFAULT_PLACEMENT.

Also accepts everything in Dialog.Props.

Types

NameValuesWhat it decides
Placement"top" | "right" | "bottom" | "left"Physical edge of the viewport the panel docks against and slides in from.