Open demo

CSS code

HTML
<div><span class="lbl">Right-side drawer</span>
<section class="dr" data-cart data-free-over="150" data-ship="8.00" aria-label="Store header with cart drawer">
  <div class="dr-store">
    <div class="dr-bar">
      <span class="dr-logo">Northbound</span>
      <nav class="dr-nav" aria-label="Shop"><a href="#">Knitwear</a><a href="#">Basics</a><a href="#">Journal</a></nav>
      <button class="dr-bag" id="dr-bag-btn" type="button" data-drawer-open aria-controls="dr-cart" aria-haspopup="dialog"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg>Bag<span class="dr-badge"><span data-badge>4</span><span class="sr-only"> items</span></span></button>
    </div>
    <div class="dr-hero"><p class="dr-kicker">Autumn edit</p><h3>The fell knit is back</h3><p>Undyed wool, knitted slowly, made to be worn for years.</p></div>
  </div>
  <dialog class="dr-drawer" id="dr-cart" aria-labelledby="dr-cart-title" data-drawer>
    <div class="dr-head"><h2 class="dr-title" id="dr-cart-title">Your bag<span data-count>4 items</span></h2>
      <button class="dr-close" type="button" data-drawer-close aria-label="Close bag"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
      <div class="dr-free"><p id="dr1-free" data-free-msg>You have unlocked free shipping.</p>
        <div class="dr-track" role="progressbar" aria-labelledby="dr1-free" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"><span class="dr-fill" data-free-bar style="width:100%"></span></div></div>
    <div class="dr-body" data-filled>
      <ul class="dr-list">
          <li class="dr-line" data-line data-name="Fell knit poncho" data-price="86.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1434389677669-e08b4cac3105?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Fell knit poncho</h3><p class="dr-meta">Oatmeal · One size · $86.00</p>
              <div class="dr-qty"><label for="dr1-q1">Qty<span class="sr-only"> of Fell knit poncho</span></label>
                <div class="dr-step"><button type="button" data-dec aria-label="Decrease quantity of Fell knit poncho"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input id="dr1-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button type="button" data-inc aria-label="Increase quantity of Fell knit poncho"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$86.00</p><button class="dr-remove" type="button" data-remove>Remove<span class="sr-only"> Fell knit poncho</span></button></div>
          </li>
          <li class="dr-line" data-line data-name="Everyday tee" data-price="28.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1581655353564-df123a1eb820?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Everyday tee</h3><p class="dr-meta">White · Size M · $28.00</p>
              <div class="dr-qty"><label for="dr1-q2">Qty<span class="sr-only"> of Everyday tee</span></label>
                <div class="dr-step"><button type="button" data-dec aria-label="Decrease quantity of Everyday tee"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input id="dr1-q2" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button type="button" data-inc aria-label="Increase quantity of Everyday tee"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$56.00</p><button class="dr-remove" type="button" data-remove>Remove<span class="sr-only"> Everyday tee</span></button></div>
          </li>
          <li class="dr-line" data-line data-name="Canvas market tote" data-price="19.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1544816155-12df9643f363?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Canvas market tote</h3><p class="dr-meta">Natural canvas · $19.00</p>
              <div class="dr-qty"><label for="dr1-q3">Qty<span class="sr-only"> of Canvas market tote</span></label>
                <div class="dr-step"><button type="button" data-dec aria-label="Decrease quantity of Canvas market tote"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input id="dr1-q3" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button type="button" data-inc aria-label="Increase quantity of Canvas market tote"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$19.00</p><button class="dr-remove" type="button" data-remove>Remove<span class="sr-only"> Canvas market tote</span></button></div>
          </li>
      </ul>
    </div>
      <div class="dr-empty" data-empty hidden>
        <div class="dr-empty-art"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
        <h3 class="dr-empty-title" tabindex="-1">Your bag is empty</h3>
        <p>Your next favourite jumper is one click away.</p>
        <a class="dr-btn" href="#">Shop knitwear</a>
      </div>
    <div class="dr-foot" data-filled>
        <dl class="dr-rows">
          <div class="dr-row"><dt>Subtotal</dt><dd data-subtotal>$161.00</dd></div>
          <div class="dr-row"><dt>Shipping</dt><dd data-shipping>Free</dd></div>
          <div class="dr-row dr-row--total"><dt>Total</dt><dd data-total>$161.00</dd></div>
        </dl>
      <p class="dr-small">Taxes are calculated at checkout.</p>
      <a class="dr-btn" href="#"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>Checkout</a>
      <a class="dr-link" href="#">View full bag</a>
    </div>
    <p class="sr-only" role="status" data-announce></p>
  </dialog>
</section>
</div>
<div><span class="lbl">Bottom sheet</span>
<section class="dr" data-cart data-free-over="200" data-ship="8.00" aria-labelledby="dr-p-title">
  <div class="dr-shop">
    <img class="dr-shop-img" src="https://images.unsplash.com/photo-1515562141207-7a88fb7ce338?w=400&h=400&fit=crop&auto=format&q=70" alt="Freshwater pearl strand on a dark velvet tray" width="200" height="240">
    <div class="dr-shop-body">
      <h3 id="dr-p-title">Freshwater pearl strand</h3>
      <p class="dr-price">$120.00</p>
      <p>Hand-knotted between every pearl, finished with a silver clasp.</p>
      <div class="dr-minibar"><p><strong data-count>2 items</strong> in your bag · <span data-subtotal>$184.00</span></p>
        <button class="dr-btn" id="dr-sheet-btn" type="button" data-drawer-open aria-controls="dr-sheet" aria-haspopup="dialog">View bag</button></div>
    </div>
  </div>
  <dialog class="dr-sheet" id="dr-sheet" aria-labelledby="dr-sheet-title" data-drawer>
    <div class="dr-grab" aria-hidden="true"></div>
    <div class="dr-head"><h2 class="dr-title" id="dr-sheet-title">Your bag<span data-count>2 items</span></h2>
      <button class="dr-close" type="button" data-drawer-close aria-label="Close bag"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
      <div class="dr-free"><p id="dr2-free" data-free-msg>Spend $16.00 more for free shipping.</p>
        <div class="dr-track" role="progressbar" aria-labelledby="dr2-free" aria-valuemin="0" aria-valuemax="100" aria-valuenow="92"><span class="dr-fill" data-free-bar style="width:92%"></span></div></div>
    <div class="dr-body" data-filled>
      <ul class="dr-list">
          <li class="dr-line" data-line data-name="Freshwater pearl strand" data-price="120.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1515562141207-7a88fb7ce338?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Freshwater pearl strand</h3><p class="dr-meta">45 cm · Silver clasp · $120.00</p>
              <div class="dr-qty"><label for="dr2-q1">Qty<span class="sr-only"> of Freshwater pearl strand</span></label>
                <div class="dr-step"><button type="button" data-dec aria-label="Decrease quantity of Freshwater pearl strand"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input id="dr2-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button type="button" data-inc aria-label="Increase quantity of Freshwater pearl strand"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$120.00</p><button class="dr-remove" type="button" data-remove>Remove<span class="sr-only"> Freshwater pearl strand</span></button></div>
          </li>
          <li class="dr-line" data-line data-name="Gold hoop earrings" data-price="64.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1617038220319-276d3cfab638?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Gold hoop earrings</h3><p class="dr-meta">Small · Gold vermeil · $64.00</p>
              <div class="dr-qty"><label for="dr2-q2">Qty<span class="sr-only"> of Gold hoop earrings</span></label>
                <div class="dr-step"><button type="button" data-dec aria-label="Decrease quantity of Gold hoop earrings"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input id="dr2-q2" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button type="button" data-inc aria-label="Increase quantity of Gold hoop earrings"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$64.00</p><button class="dr-remove" type="button" data-remove>Remove<span class="sr-only"> Gold hoop earrings</span></button></div>
          </li>
      </ul>
    </div>
      <div class="dr-empty" data-empty hidden>
        <div class="dr-empty-art"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
        <h3 class="dr-empty-title" tabindex="-1">Your bag is empty</h3>
        <p>Pieces you add will wait here for you.</p>
        <a class="dr-btn" href="#">Shop jewellery</a>
      </div>
    <div class="dr-foot" data-filled>
        <dl class="dr-rows">
          <div class="dr-row"><dt>Subtotal</dt><dd data-subtotal>$184.00</dd></div>
          <div class="dr-row"><dt>Shipping</dt><dd data-shipping>$8.00</dd></div>
          <div class="dr-row dr-row--total"><dt>Total</dt><dd data-total>$192.00</dd></div>
        </dl>
      <div class="dr-actions"><button class="dr-btn dr-btn--ghost" type="button" data-drawer-close>Keep shopping</button><a class="dr-btn" href="#">Checkout</a></div>
    </div>
    <p class="sr-only" role="status" data-announce></p>
  </dialog>
</section>
</div>
CSS
/* Cart drawer and bottom sheet built on <dialog>. showModal() gives focus trapping, Escape and a backdrop for
   free; the slide uses @starting-style, so older browsers simply show the panel without the animation. */
.dr{--dr-accent:#1e3a8a;--dr-accent-dark:#172c6b;--dr-ink:#0f172a;--dr-muted:#475569;--dr-line:#e2e8f0;--dr-field:#64748b;
  --dr-soft:#f1f5f9;--dr-ring:rgba(30,58,138,.3);--dr-ok:#166534;color:var(--dr-ink);font-size:.93rem;line-height:1.45}
.dr [hidden]{display:none!important}
.dr .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.dr :where(button){font:inherit}
.dr :is(a,button):focus-visible,.dr-empty-title:focus-visible{outline:2px solid var(--dr-accent);outline-offset:2px}
.dr-empty-title:focus:not(:focus-visible){outline:none}
/* storefront the drawer opens from */
.dr-store{background:#fff;border-radius:18px;overflow:hidden;box-shadow:0 1px 2px rgba(15,23,42,.06),0 18px 40px -22px rgba(15,23,42,.35)}
.dr-bar{display:flex;align-items:center;gap:24px;padding:14px 22px;border-bottom:1px solid var(--dr-line)}
.dr-logo{font-weight:800;letter-spacing:.16em;text-transform:uppercase;font-size:.9rem;margin-right:auto}
.dr-nav{display:flex;gap:22px}
.dr-nav a{color:var(--dr-muted);text-decoration:none;font-weight:500}
.dr-nav a:hover{color:var(--dr-ink)}
.dr-bag{display:inline-flex;align-items:center;gap:8px;padding:8px 10px 8px 14px;border:1px solid var(--dr-line);border-radius:999px;background:#fff;color:var(--dr-ink);font-weight:600;cursor:pointer}
.dr-bag:hover{border-color:var(--dr-field)}
.dr-badge{min-width:22px;height:22px;padding:0 6px;border-radius:999px;background:var(--dr-accent);color:#fff;font-size:.75rem;font-weight:700;display:inline-grid;place-items:center}
.dr-hero{padding:56px 28px;background:linear-gradient(115deg,#dfe6f2 0%,#eef0f3 55%,#f4efe6 100%)}
.dr-kicker{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--dr-muted);font-weight:600}
.dr-hero h3{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:2rem;line-height:1.1;margin:8px 0 6px}
.dr-hero p{color:var(--dr-muted);max-width:36ch}
/* the dialog panel */
.dr-drawer{position:fixed;inset:0 0 0 auto;margin:0;width:min(420px,100vw);max-width:100vw;height:100dvh;max-height:100dvh;padding:0;border:0;
  background:#fff;color:var(--dr-ink);box-shadow:-20px 0 50px -20px rgba(15,23,42,.4);translate:100% 0;
  transition:translate .32s cubic-bezier(.2,.8,.2,1),display .32s allow-discrete,overlay .32s allow-discrete}
.dr-drawer[open]{display:flex;flex-direction:column;translate:0 0}
@starting-style{.dr-drawer[open]{translate:100% 0}}
.dr-drawer::backdrop,.dr-sheet::backdrop{background:rgba(15,23,42,.5);transition:opacity .32s,display .32s allow-discrete,overlay .32s allow-discrete}
@starting-style{.dr-drawer[open]::backdrop,.dr-sheet[open]::backdrop{opacity:0}}
.dr-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:18px 22px;border-bottom:1px solid var(--dr-line)}
.dr-title{font-size:1.15rem;font-weight:700}
.dr-title span{font-weight:500;color:var(--dr-muted);font-size:.9rem;margin-left:6px}
.dr-close{width:38px;height:38px;display:grid;place-items:center;border:0;border-radius:50%;background:var(--dr-soft);color:var(--dr-ink);cursor:pointer}
.dr-close:hover{background:var(--dr-line)}
.dr-free{padding:14px 22px;background:var(--dr-soft);font-size:.85rem;font-weight:600}
.dr-track{height:6px;border-radius:99px;background:#d5dce6;margin-top:8px;overflow:hidden}
.dr-fill{display:block;height:100%;border-radius:inherit;background:var(--dr-accent);transition:width .3s}
.dr-body{flex:1;overflow-y:auto;padding:0 22px}
.dr-list{list-style:none}
.dr-line{display:grid;grid-template-columns:72px minmax(0,1fr) auto;gap:14px;padding:18px 0;border-bottom:1px solid var(--dr-line)}
.dr-line:last-child{border-bottom:0}
.dr-img{width:72px;height:88px;border-radius:10px;object-fit:cover;background:var(--dr-soft)}
.dr-name{font-size:.95rem;font-weight:600;line-height:1.3}
.dr-meta{color:var(--dr-muted);font-size:.82rem;margin-top:2px}
.dr-qty{display:flex;align-items:center;gap:8px;margin-top:10px}
.dr-qty label{font-size:.75rem;font-weight:600;color:var(--dr-muted)}
.dr-step{display:inline-flex;align-items:center;border:1px solid var(--dr-field);border-radius:8px}
.dr-step:focus-within{border-color:var(--dr-accent);box-shadow:0 0 0 3px var(--dr-ring)}
.dr-step button{width:30px;height:30px;display:grid;place-items:center;border:0;background:none;color:var(--dr-ink);border-radius:7px;cursor:pointer}
.dr-step button:hover{background:var(--dr-soft)}
.dr-step button[aria-disabled="true"]{color:#94a3b8;cursor:not-allowed;background:none}
.dr-step input{width:32px;height:30px;border:0;background:none;text-align:center;font:inherit;font-weight:600;color:inherit;-moz-appearance:textfield;appearance:textfield}
.dr-step input::-webkit-inner-spin-button,.dr-step input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
.dr-step input:focus{outline:none}
.dr-end{display:flex;flex-direction:column;align-items:flex-end;justify-content:space-between}
.dr-total{font-weight:700;font-variant-numeric:tabular-nums}
.dr-remove{border:0;background:none;padding:2px 0;color:var(--dr-muted);font-size:.82rem;text-decoration:underline;text-underline-offset:3px;cursor:pointer}
.dr-remove:hover{color:var(--dr-ink)}
.dr-foot{padding:18px 22px 22px;border-top:1px solid var(--dr-line);background:#fff}
.dr-rows{display:grid;gap:6px;margin-bottom:12px}
.dr-row{display:flex;justify-content:space-between;gap:12px}
.dr-row dt{color:var(--dr-muted)}
.dr-row dd{font-weight:600;font-variant-numeric:tabular-nums}
.dr-row--total{font-size:1.08rem;padding-top:8px;border-top:1px solid var(--dr-line);margin-top:4px}
.dr-row--total dt{color:var(--dr-ink);font-weight:700}
.dr-row--total dd{font-weight:800}
.dr-small{font-size:.78rem;color:var(--dr-muted);margin-bottom:14px}
.dr-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:13px 18px;border:0;border-radius:10px;background:var(--dr-accent);color:#fff;
  font-weight:700;text-decoration:none;cursor:pointer;transition:background-color .15s}
.dr-btn:hover{background:var(--dr-accent-dark)}
.dr-btn--ghost{background:#fff;color:var(--dr-accent);border:1px solid var(--dr-accent)}
.dr-btn--ghost:hover{background:var(--dr-soft)}
.dr-link{display:block;text-align:center;margin-top:12px;color:var(--dr-accent);font-weight:600;font-size:.86rem;text-underline-offset:3px}
.dr-empty{flex:1;display:grid;place-content:center;justify-items:center;text-align:center;padding:40px 28px;gap:8px}
.dr-empty-art{width:68px;height:68px;border-radius:50%;background:var(--dr-soft);display:grid;place-items:center;color:var(--dr-accent);margin-bottom:6px}
.dr-empty-title{font-size:1.15rem;font-weight:700}
.dr-empty p{color:var(--dr-muted);margin-bottom:10px}
/* bottom sheet */
.dr-shop{display:grid;grid-template-columns:200px minmax(0,1fr);background:#fff;border-radius:18px;overflow:hidden;
  box-shadow:0 1px 2px rgba(15,23,42,.06),0 18px 40px -22px rgba(15,23,42,.35)}
.dr-shop-img{width:100%;height:100%;min-height:220px;object-fit:cover}
.dr-shop-body{padding:24px;display:flex;flex-direction:column;gap:6px}
.dr-shop-body h3{font-size:1.2rem}
.dr-price{font-weight:700;font-size:1.05rem}
.dr-shop-body p{color:var(--dr-muted)}
.dr-minibar{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 12px 12px 16px;border-radius:12px;background:var(--dr-ink);color:#fff}
.dr-minibar p{color:#e2e8f0;font-size:.88rem}
.dr-minibar strong{color:#fff}
.dr-minibar .dr-btn{width:auto;padding:9px 16px;white-space:nowrap;background:#fff;color:var(--dr-ink)}
.dr-minibar .dr-btn:hover{background:#e2e8f0}
.dr-minibar .dr-btn:focus-visible{outline-color:#fff}
.dr-sheet{position:fixed;inset:auto 0 0 0;margin:0 auto;width:min(560px,100vw);max-width:100vw;max-height:85dvh;padding:0;border:0;border-radius:20px 20px 0 0;
  background:#fff;color:var(--dr-ink);translate:0 100%;
  transition:translate .32s cubic-bezier(.2,.8,.2,1),display .32s allow-discrete,overlay .32s allow-discrete}
.dr-sheet[open]{display:flex;flex-direction:column;translate:0 0}
@starting-style{.dr-sheet[open]{translate:0 100%}}
.dr-grab{width:44px;height:5px;border-radius:99px;background:#cbd5e1;margin:10px auto 0}
.dr-sheet .dr-head{border-bottom:0;padding-bottom:6px}
.dr-sheet .dr-free{margin:0 22px;border-radius:10px}
.dr-sheet .dr-img{height:72px}
.dr-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media (max-width:640px){.dr-nav{display:none}.dr-shop{grid-template-columns:1fr}.dr-shop-img{height:200px;min-height:0}}
@media (prefers-reduced-motion:reduce){.dr-drawer,.dr-sheet,.dr-drawer::backdrop,.dr-sheet::backdrop,.dr-fill,.dr-btn{transition:none}}
JavaScript
// Cart: quantity steppers, remove buttons and totals for every [data-cart] on the page. Prices come from
// data-price on each [data-line] and are summed in cents. Shipping is data-ship unless the subtotal reaches
// data-free-over, and [data-free-msg] / [data-free-bar] show how far off free shipping is. [data-badge]
// gets the item count. Changes are announced through the cart's polite [data-announce] region.
document.querySelectorAll('[data-cart]').forEach(function (cart) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var freeOver = cart.dataset.freeOver ? cents(cart.dataset.freeOver) : Infinity;
  var fee = cents(cart.dataset.ship || 0);
  var status = cart.querySelector('[data-announce]');
  function put(sel, text) { cart.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = 0, count = 0;
    cart.querySelectorAll('[data-line]').forEach(function (line) {
      var input = line.querySelector('[data-qty]'), min = +input.min || 1, max = +input.max || 99;
      var qty = Math.min(max, Math.max(min, parseInt(input.value, 10) || min));
      var total = cents(line.dataset.price) * qty;
      input.value = qty; sub += total; count += qty;
      line.querySelector('[data-line-total]').textContent = money.format(total / 100);
      line.querySelector('[data-dec]').setAttribute('aria-disabled', qty <= min);
      line.querySelector('[data-inc]').setAttribute('aria-disabled', qty >= max);
    });
    var ship = sub === 0 || sub >= freeOver ? 0 : fee;
    put('[data-subtotal]', money.format(sub / 100));
    put('[data-shipping]', ship ? money.format(ship / 100) : 'Free');
    put('[data-total]', money.format((sub + ship) / 100));
    put('[data-count]', count + (count === 1 ? ' item' : ' items'));
    put('[data-badge]', String(count));
    put('[data-free-msg]', sub >= freeOver ? 'You have unlocked free shipping.' : 'Spend ' + money.format((freeOver - sub) / 100) + ' more for free shipping.');
    cart.querySelectorAll('[data-free-bar]').forEach(function (bar) {
      var pct = Math.min(100, Math.round(sub / freeOver * 100));
      bar.style.width = pct + '%';
      bar.parentElement.setAttribute('aria-valuenow', pct);
    });
    cart.querySelectorAll('[data-filled]').forEach(function (el) { el.hidden = !count; });
    cart.querySelectorAll('[data-empty]').forEach(function (el) { el.hidden = !!count; });
    return { sub: sub, count: count };
  }
  function announce(text) { if (status) status.textContent = text; }
  cart.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-inc],[data-dec],[data-remove]');
    if (!btn) return;
    var line = btn.closest('[data-line]'), name = line.dataset.name, input = line.querySelector('[data-qty]');
    if (btn.hasAttribute('data-remove')) {
      var next = line.nextElementSibling || line.previousElementSibling;
      line.remove();
      var r = render();
      announce('Removed ' + name + '. ' + (r.count ? 'Subtotal ' + money.format(r.sub / 100) + '.' : 'Your cart is empty.'));
      var target = r.count ? next.querySelector('[data-remove]') : cart.querySelector('[data-empty] [tabindex="-1"]');
      if (target) target.focus();
      return;
    }
    if (btn.getAttribute('aria-disabled') === 'true') return;
    input.value = (parseInt(input.value, 10) || 1) + (btn.hasAttribute('data-inc') ? 1 : -1);
    var s = render();
    announce(name + ', quantity ' + input.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  cart.addEventListener('change', function (e) {
    if (!e.target.matches('[data-qty]')) return;
    var s = render();
    announce(e.target.closest('[data-line]').dataset.name + ', quantity ' + e.target.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  render();
});

// Drawer: a [data-drawer-open] button opens the <dialog> named by its aria-controls with showModal(), which
// keeps focus inside and closes on Escape. The backdrop and [data-drawer-close] buttons close it too, and focus
// goes back to the button that opened it.
document.querySelectorAll('[data-drawer-open]').forEach(function (btn) {
  var dialog = document.getElementById(btn.getAttribute('aria-controls'));
  btn.addEventListener('click', function () { dialog.showModal(); dialog.dataset.opener = btn.id; });
});
document.querySelectorAll('dialog[data-drawer]').forEach(function (dialog) {
  dialog.addEventListener('click', function (e) {
    if (e.target === dialog || e.target.closest('[data-drawer-close]')) dialog.close();
  });
  dialog.addEventListener('close', function () {
    var opener = document.getElementById(dialog.dataset.opener || '');
    if (opener) opener.focus();
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div><span class="lbl">Right-side drawer</span>
<section class="dr" data-cart data-free-over="150" data-ship="8.00" aria-label="Store header with cart drawer">
  <div class="dr-store">
    <div class="dr-bar">
      <span class="dr-logo">Northbound</span>
      <nav aria-label="Shop"><ul class="nav"><li class="nav-item"><a class="nav-link" href="#">Knitwear</a></li><li class="nav-item"><a class="nav-link" href="#">Basics</a></li><li class="nav-item"><a class="nav-link" href="#">Journal</a></li></ul></nav>
      <button class="btn dr-bag" id="dr-bag-btn" type="button" data-bs-toggle="offcanvas" data-bs-target="#dr-cart" aria-controls="dr-cart"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg>Bag<span class="badge rounded-pill"><span data-badge>4</span><span class="visually-hidden"> items</span></span></button>
    </div>
    <div class="dr-hero"><p class="dr-kicker">Autumn edit</p><h3>The fell knit is back</h3><p>Undyed wool, knitted slowly, made to be worn for years.</p></div>
  </div>
  <div class="offcanvas offcanvas-end" tabindex="-1" id="dr-cart" aria-labelledby="dr-cart-title">
    <div class="offcanvas-header justify-content-between gap-3"><h2 class="offcanvas-title dr-title" id="dr-cart-title">Your bag<span data-count>4 items</span></h2>
      <button class="btn dr-close" type="button" data-bs-dismiss="offcanvas" aria-label="Close bag"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
      <div class="dr-free"><p id="dr1-free" data-free-msg>You have unlocked free shipping.</p>
        <div class="progress" role="progressbar" aria-labelledby="dr1-free" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"><div class="progress-bar" data-free-bar style="width:100%"></div></div></div>
    <div class="offcanvas-body" data-filled>
      <ul class="list-group">
          <li class="list-group-item dr-line" data-line data-name="Fell knit poncho" data-price="86.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1434389677669-e08b4cac3105?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Fell knit poncho</h3><p class="dr-meta">Oatmeal · One size · $86.00</p>
              <div class="dr-qty"><label class="form-label" for="dr1-q1">Qty<span class="visually-hidden"> of Fell knit poncho</span></label>
                <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Fell knit poncho"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="dr1-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Fell knit poncho"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$86.00</p><button class="btn btn-link dr-remove" type="button" data-remove>Remove<span class="visually-hidden"> Fell knit poncho</span></button></div>
          </li>
          <li class="list-group-item dr-line" data-line data-name="Everyday tee" data-price="28.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1581655353564-df123a1eb820?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Everyday tee</h3><p class="dr-meta">White · Size M · $28.00</p>
              <div class="dr-qty"><label class="form-label" for="dr1-q2">Qty<span class="visually-hidden"> of Everyday tee</span></label>
                <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Everyday tee"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="dr1-q2" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Everyday tee"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$56.00</p><button class="btn btn-link dr-remove" type="button" data-remove>Remove<span class="visually-hidden"> Everyday tee</span></button></div>
          </li>
          <li class="list-group-item dr-line" data-line data-name="Canvas market tote" data-price="19.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1544816155-12df9643f363?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Canvas market tote</h3><p class="dr-meta">Natural canvas · $19.00</p>
              <div class="dr-qty"><label class="form-label" for="dr1-q3">Qty<span class="visually-hidden"> of Canvas market tote</span></label>
                <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Canvas market tote"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="dr1-q3" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Canvas market tote"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$19.00</p><button class="btn btn-link dr-remove" type="button" data-remove>Remove<span class="visually-hidden"> Canvas market tote</span></button></div>
          </li>
      </ul>
    </div>
      <div class="dr-empty" data-empty hidden>
        <div class="dr-empty-art"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
        <h3 class="dr-empty-title" tabindex="-1">Your bag is empty</h3>
        <p>Your next favourite jumper is one click away.</p>
        <a class="btn btn-primary" href="#">Shop knitwear</a>
      </div>
    <div class="dr-foot" data-filled>
        <dl class="dr-rows">
          <div class="dr-row"><dt>Subtotal</dt><dd data-subtotal>$161.00</dd></div>
          <div class="dr-row"><dt>Shipping</dt><dd data-shipping>Free</dd></div>
          <div class="dr-row dr-row--total"><dt>Total</dt><dd data-total>$161.00</dd></div>
        </dl>
      <p class="dr-small">Taxes are calculated at checkout.</p>
      <a class="btn btn-primary" href="#"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>Checkout</a>
      <a class="dr-link" href="#">View full bag</a>
    </div>
    <p class="visually-hidden" role="status" data-announce></p>
  </div>
</section>
</div>
<div><span class="lbl">Bottom sheet</span>
<section class="dr" data-cart data-free-over="200" data-ship="8.00" aria-labelledby="dr-p-title">
  <div class="dr-shop">
    <img class="dr-shop-img" src="https://images.unsplash.com/photo-1515562141207-7a88fb7ce338?w=400&h=400&fit=crop&auto=format&q=70" alt="Freshwater pearl strand on a dark velvet tray" width="200" height="240">
    <div class="dr-shop-body">
      <h3 id="dr-p-title">Freshwater pearl strand</h3>
      <p class="dr-price">$120.00</p>
      <p>Hand-knotted between every pearl, finished with a silver clasp.</p>
      <div class="dr-minibar"><p><strong data-count>2 items</strong> in your bag · <span data-subtotal>$184.00</span></p>
        <button class="btn btn-light" id="dr-sheet-btn" type="button" data-bs-toggle="offcanvas" data-bs-target="#dr-sheet" aria-controls="dr-sheet">View bag</button></div>
    </div>
  </div>
  <div class="offcanvas offcanvas-bottom" tabindex="-1" id="dr-sheet" aria-labelledby="dr-sheet-title">
    <div class="dr-grab" aria-hidden="true"></div>
    <div class="offcanvas-header justify-content-between gap-3"><h2 class="offcanvas-title dr-title" id="dr-sheet-title">Your bag<span data-count>2 items</span></h2>
      <button class="btn dr-close" type="button" data-bs-dismiss="offcanvas" aria-label="Close bag"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
      <div class="dr-free"><p id="dr2-free" data-free-msg>Spend $16.00 more for free shipping.</p>
        <div class="progress" role="progressbar" aria-labelledby="dr2-free" aria-valuemin="0" aria-valuemax="100" aria-valuenow="92"><div class="progress-bar" data-free-bar style="width:92%"></div></div></div>
    <div class="offcanvas-body" data-filled>
      <ul class="list-group">
          <li class="list-group-item dr-line" data-line data-name="Freshwater pearl strand" data-price="120.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1515562141207-7a88fb7ce338?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Freshwater pearl strand</h3><p class="dr-meta">45 cm · Silver clasp · $120.00</p>
              <div class="dr-qty"><label class="form-label" for="dr2-q1">Qty<span class="visually-hidden"> of Freshwater pearl strand</span></label>
                <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Freshwater pearl strand"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="dr2-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Freshwater pearl strand"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$120.00</p><button class="btn btn-link dr-remove" type="button" data-remove>Remove<span class="visually-hidden"> Freshwater pearl strand</span></button></div>
          </li>
          <li class="list-group-item dr-line" data-line data-name="Gold hoop earrings" data-price="64.00">
            <img class="dr-img" src="https://images.unsplash.com/photo-1617038220319-276d3cfab638?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="dr-name">Gold hoop earrings</h3><p class="dr-meta">Small · Gold vermeil · $64.00</p>
              <div class="dr-qty"><label class="form-label" for="dr2-q2">Qty<span class="visually-hidden"> of Gold hoop earrings</span></label>
                <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Gold hoop earrings"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="dr2-q2" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Gold hoop earrings"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="dr-end"><p class="dr-total" data-line-total>$64.00</p><button class="btn btn-link dr-remove" type="button" data-remove>Remove<span class="visually-hidden"> Gold hoop earrings</span></button></div>
          </li>
      </ul>
    </div>
      <div class="dr-empty" data-empty hidden>
        <div class="dr-empty-art"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
        <h3 class="dr-empty-title" tabindex="-1">Your bag is empty</h3>
        <p>Pieces you add will wait here for you.</p>
        <a class="btn btn-primary" href="#">Shop jewellery</a>
      </div>
    <div class="dr-foot" data-filled>
        <dl class="dr-rows">
          <div class="dr-row"><dt>Subtotal</dt><dd data-subtotal>$184.00</dd></div>
          <div class="dr-row"><dt>Shipping</dt><dd data-shipping>$8.00</dd></div>
          <div class="dr-row dr-row--total"><dt>Total</dt><dd data-total>$192.00</dd></div>
        </dl>
      <div class="dr-actions"><button class="btn btn-outline-primary" type="button" data-bs-dismiss="offcanvas">Keep shopping</button><a class="btn btn-primary" href="#">Checkout</a></div>
    </div>
    <p class="visually-hidden" role="status" data-announce></p>
  </div>
</section>
</div>
CSS
/* Bootstrap .offcanvas (end and bottom) with .list-group lines, .input-group steppers and a .progress bar,
   recoloured through Bootstrap's CSS variables. Bootstrap's offcanvas handles the focus trap, Escape and backdrop. */
.dr{--dr-accent:#1e3a8a;--dr-accent-dark:#172c6b;--dr-ink:#0f172a;--dr-muted:#475569;--dr-line:#e2e8f0;--dr-field:#64748b;--dr-soft:#f1f5f9;--dr-ring:rgba(30,58,138,.3);
  --bs-body-color:var(--dr-ink);--bs-secondary-color:var(--dr-muted);--bs-border-color:var(--dr-line);--bs-link-color-rgb:30,58,138;--bs-link-hover-color-rgb:23,44,107;
  color:var(--dr-ink);font-size:.93rem;line-height:1.45}
.dr p,.dr h2,.dr h3,.dr dl,.dr dd{margin:0}
.dr .btn:focus-visible,.dr a:focus-visible,.dr-empty-title:focus-visible{outline:2px solid var(--dr-accent);outline-offset:2px;box-shadow:none}
.dr-empty-title:focus:not(:focus-visible){outline:none}
.dr-store{background:#fff;border-radius:18px;overflow:hidden;box-shadow:0 1px 2px rgba(15,23,42,.06),0 18px 40px -22px rgba(15,23,42,.35)}
.dr-bar{display:flex;align-items:center;gap:24px;padding:14px 22px;border-bottom:1px solid var(--dr-line)}
.dr-logo{font-weight:800;letter-spacing:.16em;text-transform:uppercase;font-size:.9rem;margin-right:auto}
.dr .nav{--bs-nav-link-color:var(--dr-muted);--bs-nav-link-hover-color:var(--dr-ink);--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0;--bs-nav-link-font-weight:500;gap:22px}
.dr-bag{--bs-btn-padding-x:14px;--bs-btn-padding-y:8px;--bs-btn-border-color:var(--dr-line);--bs-btn-hover-border-color:var(--dr-field);--bs-btn-color:var(--dr-ink);--bs-btn-hover-color:var(--dr-ink);
  --bs-btn-active-border-color:var(--dr-field);--bs-btn-font-weight:600;--bs-btn-border-radius:999px;display:inline-flex;align-items:center;gap:8px;padding-right:10px}
.dr .badge{--bs-badge-font-size:.75rem;--bs-badge-padding-x:6px;min-width:22px;height:22px;display:inline-grid;place-items:center;background:var(--dr-accent)}
.dr-hero{padding:56px 28px;background:linear-gradient(115deg,#dfe6f2 0%,#eef0f3 55%,#f4efe6 100%)}
.dr-kicker{font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--dr-muted);font-weight:600}
.dr-hero h3{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:2rem;line-height:1.1;margin:8px 0 6px}
.dr-hero p{color:var(--dr-muted);max-width:36ch}
.dr .offcanvas{--bs-offcanvas-width:420px;--bs-offcanvas-padding-x:22px;--bs-offcanvas-padding-y:18px;--bs-offcanvas-color:var(--dr-ink);--bs-offcanvas-border-width:0;
  --bs-offcanvas-transition:transform .32s cubic-bezier(.2,.8,.2,1);max-width:100vw}
.dr .offcanvas-end{box-shadow:-20px 0 50px -20px rgba(15,23,42,.4)}
.dr .offcanvas-header{border-bottom:1px solid var(--dr-line)}
.dr-title{font-size:1.15rem;font-weight:700}
.dr-title span{font-weight:500;color:var(--dr-muted);font-size:.9rem;margin-left:6px}
.dr-close{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-bg:var(--dr-soft);--bs-btn-hover-bg:var(--dr-line);--bs-btn-active-bg:var(--dr-line);--bs-btn-color:var(--dr-ink);--bs-btn-hover-color:var(--dr-ink);
  --bs-btn-border-width:0;width:38px;height:38px;display:grid;place-items:center;border-radius:50%}
.dr-free{padding:14px 22px;background:var(--dr-soft);font-size:.85rem;font-weight:600}
.dr .progress{--bs-progress-height:6px;--bs-progress-bg:#d5dce6;--bs-progress-bar-bg:var(--dr-accent);--bs-progress-border-radius:99px;margin-top:8px}
.dr .offcanvas-body{padding-top:0;padding-bottom:0}
.dr .list-group{--bs-list-group-border-width:0;--bs-list-group-item-padding-x:0;--bs-list-group-item-padding-y:18px;--bs-list-group-bg:transparent;--bs-list-group-color:var(--dr-ink)}
.dr-line{display:grid!important;grid-template-columns:72px minmax(0,1fr) auto;gap:14px;border-bottom:1px solid var(--dr-line)!important}
.dr-line:last-child{border-bottom:0!important}
.dr-img{width:72px;height:88px;border-radius:10px;object-fit:cover;background:var(--dr-soft)}
.dr-name{font-size:.95rem;font-weight:600;line-height:1.3}
.dr-meta{color:var(--dr-muted);font-size:.82rem;margin-top:2px!important}
.dr-qty{display:flex;align-items:center;gap:8px;margin-top:10px}
.dr-qty .form-label{font-size:.75rem;font-weight:600;color:var(--dr-muted);margin:0}
.dr .input-group{width:auto;flex-wrap:nowrap;border:1px solid var(--dr-field);border-radius:8px}
.dr .input-group:focus-within{border-color:var(--dr-accent);box-shadow:0 0 0 3px var(--dr-ring)}
.dr .input-group .btn{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-color:var(--dr-ink);--bs-btn-hover-bg:var(--dr-soft);--bs-btn-hover-color:var(--dr-ink);--bs-btn-active-bg:var(--dr-line);
  --bs-btn-border-width:0;width:30px;height:30px;display:grid;place-items:center;border-radius:7px!important;z-index:0}
.dr .input-group .btn[aria-disabled="true"]{color:#94a3b8;cursor:not-allowed;background:none}
.dr .input-group .form-control{flex:none;width:32px;height:30px;padding:0;border:0;background:none;text-align:center;font-weight:600;color:inherit;box-shadow:none;
  -moz-appearance:textfield;appearance:textfield}
.dr .input-group .form-control::-webkit-inner-spin-button,.dr .input-group .form-control::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
.dr-end{display:flex;flex-direction:column;align-items:flex-end;justify-content:space-between}
.dr-total{font-weight:700;font-variant-numeric:tabular-nums}
.dr-remove{--bs-btn-padding-x:0;--bs-btn-padding-y:2px;--bs-btn-font-size:.82rem;--bs-btn-color:var(--dr-muted);--bs-btn-hover-color:var(--dr-ink);--bs-btn-active-color:var(--dr-ink);--bs-btn-focus-box-shadow:none;text-underline-offset:3px}
.dr-foot{padding:18px 22px 22px;border-top:1px solid var(--dr-line);background:#fff}
.dr-rows{display:grid;gap:6px;margin-bottom:12px!important}
.dr-row{display:flex;justify-content:space-between;gap:12px}
.dr-row dt{color:var(--dr-muted);font-weight:400}
.dr-row dd{font-weight:600;font-variant-numeric:tabular-nums}
.dr-row--total{font-size:1.08rem;padding-top:8px;border-top:1px solid var(--dr-line);margin-top:4px}
.dr-row--total dt{color:var(--dr-ink);font-weight:700}
.dr-row--total dd{font-weight:800}
.dr-small{font-size:.78rem;color:var(--dr-muted);margin-bottom:14px!important}
.dr .btn-primary{--bs-btn-bg:var(--dr-accent);--bs-btn-border-color:var(--dr-accent);--bs-btn-hover-bg:var(--dr-accent-dark);--bs-btn-hover-border-color:var(--dr-accent-dark);
  --bs-btn-active-bg:var(--dr-accent-dark);--bs-btn-active-border-color:var(--dr-accent-dark);--bs-btn-padding-y:12px;--bs-btn-font-weight:700;--bs-btn-border-radius:10px;
  display:flex;align-items:center;justify-content:center;gap:8px;width:100%}
.dr .btn-outline-primary{--bs-btn-color:var(--dr-accent);--bs-btn-border-color:var(--dr-accent);--bs-btn-hover-bg:var(--dr-soft);--bs-btn-hover-color:var(--dr-accent);--bs-btn-hover-border-color:var(--dr-accent);
  --bs-btn-active-bg:var(--dr-line);--bs-btn-active-color:var(--dr-accent);--bs-btn-active-border-color:var(--dr-accent);--bs-btn-padding-y:12px;--bs-btn-font-weight:700;--bs-btn-border-radius:10px;width:100%}
.dr-link{display:block;text-align:center;margin-top:12px;font-weight:600;font-size:.86rem;text-underline-offset:3px}
.dr-empty{flex:1;display:grid;place-content:center;justify-items:center;text-align:center;padding:40px 28px;gap:8px}
.dr-empty-art{width:68px;height:68px;border-radius:50%;background:var(--dr-soft);display:grid;place-items:center;color:var(--dr-accent);margin-bottom:6px}
.dr-empty-title{font-size:1.15rem;font-weight:700}
.dr-empty p{color:var(--dr-muted);margin-bottom:10px}
.dr-shop{display:grid;grid-template-columns:200px minmax(0,1fr);background:#fff;border-radius:18px;overflow:hidden;box-shadow:0 1px 2px rgba(15,23,42,.06),0 18px 40px -22px rgba(15,23,42,.35)}
.dr-shop-img{width:100%;height:100%;min-height:220px;object-fit:cover}
.dr-shop-body{padding:24px;display:flex;flex-direction:column;gap:6px}
.dr-shop-body h3{font-size:1.2rem;font-weight:700}
.dr-price{font-weight:700;font-size:1.05rem}
.dr-shop-body>p:not(.dr-price){color:var(--dr-muted)}
.dr-minibar{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 12px 12px 16px;border-radius:12px;background:var(--dr-ink)}
.dr-minibar p{color:#e2e8f0;font-size:.88rem}
.dr-minibar strong{color:#fff}
.dr-minibar .btn-light{white-space:nowrap;--bs-btn-padding-x:16px;--bs-btn-padding-y:9px;--bs-btn-font-weight:700;--bs-btn-border-radius:10px;--bs-btn-color:var(--dr-ink);--bs-btn-bg:#fff;--bs-btn-border-color:#fff;--bs-btn-hover-bg:#e2e8f0}
.dr-minibar .btn-light:focus-visible{outline-color:#fff}
.dr .offcanvas-bottom{--bs-offcanvas-height:auto;max-height:85dvh;width:min(560px,100vw);margin:0 auto;border-radius:20px 20px 0 0}
.dr-grab{width:44px;height:5px;border-radius:99px;background:#cbd5e1;margin:10px auto 0}
.dr .offcanvas-bottom .offcanvas-header{border-bottom:0;padding-bottom:6px}
.dr .offcanvas-bottom .dr-free{margin:0 22px;border-radius:10px}
.dr .offcanvas-bottom .dr-img{height:72px}
.dr-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.offcanvas-backdrop{--bs-backdrop-bg:#0f172a;--bs-backdrop-opacity:.5}
@media (max-width:640px){.dr .nav{display:none}.dr-shop{grid-template-columns:1fr}.dr-shop-img{height:200px;min-height:0}}
@media (prefers-reduced-motion:reduce){.dr .offcanvas,.dr .progress-bar,.dr .btn{transition:none}}
JavaScript
// Cart: quantity steppers, remove buttons and totals for every [data-cart] on the page. Prices come from
// data-price on each [data-line] and are summed in cents. Shipping is data-ship unless the subtotal reaches
// data-free-over, and [data-free-msg] / [data-free-bar] show how far off free shipping is. [data-badge]
// gets the item count. Changes are announced through the cart's polite [data-announce] region.
document.querySelectorAll('[data-cart]').forEach(function (cart) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var freeOver = cart.dataset.freeOver ? cents(cart.dataset.freeOver) : Infinity;
  var fee = cents(cart.dataset.ship || 0);
  var status = cart.querySelector('[data-announce]');
  function put(sel, text) { cart.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = 0, count = 0;
    cart.querySelectorAll('[data-line]').forEach(function (line) {
      var input = line.querySelector('[data-qty]'), min = +input.min || 1, max = +input.max || 99;
      var qty = Math.min(max, Math.max(min, parseInt(input.value, 10) || min));
      var total = cents(line.dataset.price) * qty;
      input.value = qty; sub += total; count += qty;
      line.querySelector('[data-line-total]').textContent = money.format(total / 100);
      line.querySelector('[data-dec]').setAttribute('aria-disabled', qty <= min);
      line.querySelector('[data-inc]').setAttribute('aria-disabled', qty >= max);
    });
    var ship = sub === 0 || sub >= freeOver ? 0 : fee;
    put('[data-subtotal]', money.format(sub / 100));
    put('[data-shipping]', ship ? money.format(ship / 100) : 'Free');
    put('[data-total]', money.format((sub + ship) / 100));
    put('[data-count]', count + (count === 1 ? ' item' : ' items'));
    put('[data-badge]', String(count));
    put('[data-free-msg]', sub >= freeOver ? 'You have unlocked free shipping.' : 'Spend ' + money.format((freeOver - sub) / 100) + ' more for free shipping.');
    cart.querySelectorAll('[data-free-bar]').forEach(function (bar) {
      var pct = Math.min(100, Math.round(sub / freeOver * 100));
      bar.style.width = pct + '%';
      bar.parentElement.setAttribute('aria-valuenow', pct);
    });
    cart.querySelectorAll('[data-filled]').forEach(function (el) { el.hidden = !count; });
    cart.querySelectorAll('[data-empty]').forEach(function (el) { el.hidden = !!count; });
    return { sub: sub, count: count };
  }
  function announce(text) { if (status) status.textContent = text; }
  cart.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-inc],[data-dec],[data-remove]');
    if (!btn) return;
    var line = btn.closest('[data-line]'), name = line.dataset.name, input = line.querySelector('[data-qty]');
    if (btn.hasAttribute('data-remove')) {
      var next = line.nextElementSibling || line.previousElementSibling;
      line.remove();
      var r = render();
      announce('Removed ' + name + '. ' + (r.count ? 'Subtotal ' + money.format(r.sub / 100) + '.' : 'Your cart is empty.'));
      var target = r.count ? next.querySelector('[data-remove]') : cart.querySelector('[data-empty] [tabindex="-1"]');
      if (target) target.focus();
      return;
    }
    if (btn.getAttribute('aria-disabled') === 'true') return;
    input.value = (parseInt(input.value, 10) || 1) + (btn.hasAttribute('data-inc') ? 1 : -1);
    var s = render();
    announce(name + ', quantity ' + input.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  cart.addEventListener('change', function (e) {
    if (!e.target.matches('[data-qty]')) return;
    var s = render();
    announce(e.target.closest('[data-line]').dataset.name + ', quantity ' + e.target.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  render();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4b5563] uppercase">Right-side drawer</span>
<section class="text-[.93rem] leading-[1.45] text-dr-ink" data-cart data-free-over="150" data-ship="8.00" aria-label="Store header with cart drawer">
  <div class="overflow-hidden rounded-[18px] bg-white shadow-[0_1px_2px_rgba(15,23,42,.06),0_18px_40px_-22px_rgba(15,23,42,.35)]">
    <div class="flex items-center gap-6 border-b border-dr-line px-[22px] py-3.5">
      <span class="mr-auto text-[.9rem] font-extrabold tracking-[.16em] uppercase">Northbound</span>
      <nav class="flex gap-[22px] max-[640px]:hidden" aria-label="Shop"><a class="font-medium text-dr-muted hover:text-dr-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" href="#">Knitwear</a><a class="font-medium text-dr-muted hover:text-dr-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" href="#">Basics</a><a class="font-medium text-dr-muted hover:text-dr-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" href="#">Journal</a></nav>
      <button class="inline-flex cursor-pointer items-center gap-2 rounded-full border border-dr-line bg-white py-2 pr-2.5 pl-3.5 font-semibold hover:border-dr-field focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" id="dr-bag-btn" type="button" data-drawer-open aria-controls="dr-cart" aria-haspopup="dialog"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg>Bag<span class="inline-grid h-[22px] min-w-[22px] place-items-center rounded-full bg-dr-accent px-1.5 text-[.75rem] font-bold text-white"><span data-badge>4</span><span class="sr-only"> items</span></span></button>
    </div>
    <div class="bg-[linear-gradient(115deg,#dfe6f2_0%,#eef0f3_55%,#f4efe6_100%)] px-7 py-14"><p class="text-[.75rem] font-semibold tracking-[.14em] text-dr-muted uppercase">Autumn edit</p><h3 class="mt-2 mb-1.5 font-[Georgia,'Times_New_Roman',serif] text-[2rem] leading-[1.1] font-normal">The fell knit is back</h3><p class="max-w-[36ch] text-dr-muted">Undyed wool, knitted slowly, made to be worn for years.</p></div>
  </div>
  <dialog class="fixed inset-y-0 right-0 left-auto m-0 h-dvh max-h-dvh w-[min(420px,100vw)] max-w-[100vw] translate-x-full bg-white text-dr-ink shadow-[-20px_0_50px_-20px_rgba(15,23,42,.4)] transition-[translate,display,overlay] transition-discrete duration-[320ms] ease-[cubic-bezier(.2,.8,.2,1)] motion-reduce:transition-none backdrop:bg-slate-900/50 open:flex open:translate-x-0 open:flex-col starting:open:translate-x-full" id="dr-cart" aria-labelledby="dr-cart-title" data-drawer>
    <div class="flex items-center justify-between gap-3 border-b border-dr-line px-[22px] py-[18px]"><h2 class="text-[1.15rem] font-bold" id="dr-cart-title">Your bag<span class="ml-1.5 text-[.9rem] font-medium text-dr-muted" data-count>4 items</span></h2>
      <button class="grid size-[38px] cursor-pointer place-items-center rounded-full bg-dr-soft text-dr-ink hover:bg-dr-line focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-drawer-close aria-label="Close bag"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
    <div class="bg-dr-soft px-[22px] py-3.5 text-[.85rem] font-semibold "><p id="dr1-free" data-free-msg>You have unlocked free shipping.</p>
      <div class="mt-2 h-1.5 overflow-hidden rounded-full bg-[#d5dce6]" role="progressbar" aria-labelledby="dr1-free" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"><span class="block h-full rounded-[inherit] bg-dr-accent transition-[width] duration-300 motion-reduce:transition-none" data-free-bar style="width:100%"></span></div></div>
    <div class="flex-1 overflow-y-auto px-[22px]" data-filled>
      <ul>
          <li class="grid grid-cols-[72px_minmax(0,1fr)_auto] gap-3.5 border-b border-dr-line py-[18px] last:border-b-0" data-line data-name="Fell knit poncho" data-price="86.00">
            <img class="h-[88px] w-[72px] rounded-[10px] bg-dr-soft object-cover" src="https://images.unsplash.com/photo-1434389677669-e08b4cac3105?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="text-[.95rem] leading-[1.3] font-semibold">Fell knit poncho</h3><p class="mt-0.5 text-[.82rem] text-dr-muted">Oatmeal · One size · $86.00</p>
              <div class="mt-2.5 flex items-center gap-2"><label class="text-[.75rem] font-semibold text-dr-muted" for="dr1-q1">Qty<span class="sr-only"> of Fell knit poncho</span></label>
                <div class="inline-flex items-center rounded-lg border border-dr-field focus-within:border-dr-accent focus-within:shadow-[0_0_0_3px_rgba(30,58,138,.3)]"><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-dec aria-label="Decrease quantity of Fell knit poncho"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[30px] w-8 appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="dr1-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-inc aria-label="Increase quantity of Fell knit poncho"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="flex flex-col items-end justify-between"><p class="font-bold tabular-nums" data-line-total>$86.00</p><button class="cursor-pointer py-0.5 text-[.82rem] text-dr-muted underline underline-offset-[3px] hover:text-dr-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-remove>Remove<span class="sr-only"> Fell knit poncho</span></button></div>
          </li>
          <li class="grid grid-cols-[72px_minmax(0,1fr)_auto] gap-3.5 border-b border-dr-line py-[18px] last:border-b-0" data-line data-name="Everyday tee" data-price="28.00">
            <img class="h-[88px] w-[72px] rounded-[10px] bg-dr-soft object-cover" src="https://images.unsplash.com/photo-1581655353564-df123a1eb820?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="text-[.95rem] leading-[1.3] font-semibold">Everyday tee</h3><p class="mt-0.5 text-[.82rem] text-dr-muted">White · Size M · $28.00</p>
              <div class="mt-2.5 flex items-center gap-2"><label class="text-[.75rem] font-semibold text-dr-muted" for="dr1-q2">Qty<span class="sr-only"> of Everyday tee</span></label>
                <div class="inline-flex items-center rounded-lg border border-dr-field focus-within:border-dr-accent focus-within:shadow-[0_0_0_3px_rgba(30,58,138,.3)]"><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-dec aria-label="Decrease quantity of Everyday tee"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[30px] w-8 appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="dr1-q2" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-inc aria-label="Increase quantity of Everyday tee"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="flex flex-col items-end justify-between"><p class="font-bold tabular-nums" data-line-total>$56.00</p><button class="cursor-pointer py-0.5 text-[.82rem] text-dr-muted underline underline-offset-[3px] hover:text-dr-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-remove>Remove<span class="sr-only"> Everyday tee</span></button></div>
          </li>
          <li class="grid grid-cols-[72px_minmax(0,1fr)_auto] gap-3.5 border-b border-dr-line py-[18px] last:border-b-0" data-line data-name="Canvas market tote" data-price="19.00">
            <img class="h-[88px] w-[72px] rounded-[10px] bg-dr-soft object-cover" src="https://images.unsplash.com/photo-1544816155-12df9643f363?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="text-[.95rem] leading-[1.3] font-semibold">Canvas market tote</h3><p class="mt-0.5 text-[.82rem] text-dr-muted">Natural canvas · $19.00</p>
              <div class="mt-2.5 flex items-center gap-2"><label class="text-[.75rem] font-semibold text-dr-muted" for="dr1-q3">Qty<span class="sr-only"> of Canvas market tote</span></label>
                <div class="inline-flex items-center rounded-lg border border-dr-field focus-within:border-dr-accent focus-within:shadow-[0_0_0_3px_rgba(30,58,138,.3)]"><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-dec aria-label="Decrease quantity of Canvas market tote"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[30px] w-8 appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="dr1-q3" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-inc aria-label="Increase quantity of Canvas market tote"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="flex flex-col items-end justify-between"><p class="font-bold tabular-nums" data-line-total>$19.00</p><button class="cursor-pointer py-0.5 text-[.82rem] text-dr-muted underline underline-offset-[3px] hover:text-dr-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-remove>Remove<span class="sr-only"> Canvas market tote</span></button></div>
          </li>
      </ul>
    </div>
    <div class="grid flex-1 place-content-center justify-items-center gap-2 px-7 py-10 text-center" data-empty hidden>
      <div class="mb-1.5 grid size-[68px] place-items-center rounded-full bg-dr-soft text-dr-accent"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
      <h3 class="text-[1.15rem] font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" tabindex="-1">Your bag is empty</h3>
      <p class="mb-2.5 text-dr-muted">Your next favourite jumper is one click away.</p>
      <a class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] bg-dr-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-dr-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" href="#">Shop knitwear</a>
    </div>
    <div class="border-t border-dr-line bg-white px-[22px] pt-[18px] pb-[22px]" data-filled>
      <dl class="mb-3 grid gap-1.5">
        <div class="flex justify-between gap-3"><dt class="text-dr-muted">Subtotal</dt><dd class="font-semibold tabular-nums" data-subtotal>$161.00</dd></div>
        <div class="flex justify-between gap-3"><dt class="text-dr-muted">Shipping</dt><dd class="font-semibold tabular-nums" data-shipping>Free</dd></div>
        <div class="mt-1 flex justify-between gap-3 border-t border-dr-line pt-2 text-[1.08rem]"><dt class="font-bold">Total</dt><dd class="font-extrabold tabular-nums" data-total>$161.00</dd></div>
      </dl>
      <p class="mb-3.5 text-[.78rem] text-dr-muted">Taxes are calculated at checkout.</p>
      <a class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] bg-dr-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-dr-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" href="#"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>Checkout</a>
      <a class="mt-3 block text-center text-[.86rem] font-semibold text-dr-accent underline underline-offset-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" href="#">View full bag</a>
    </div>
    <p class="sr-only" role="status" data-announce></p>
  </dialog>
</section>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4b5563] uppercase">Bottom sheet</span>
<section class="text-[.93rem] leading-[1.45] text-dr-ink" data-cart data-free-over="200" data-ship="8.00" aria-labelledby="dr-p-title">
  <div class="grid grid-cols-[200px_minmax(0,1fr)] max-[640px]:grid-cols-1 overflow-hidden rounded-[18px] bg-white shadow-[0_1px_2px_rgba(15,23,42,.06),0_18px_40px_-22px_rgba(15,23,42,.35)]">
    <img class="size-full min-h-[220px] object-cover max-[640px]:h-[200px] max-[640px]:min-h-0" src="https://images.unsplash.com/photo-1515562141207-7a88fb7ce338?w=400&h=400&fit=crop&auto=format&q=70" alt="Freshwater pearl strand on a dark velvet tray" width="200" height="240">
    <div class="flex flex-col gap-1.5 p-6">
      <h3 class="text-[1.2rem] font-bold" id="dr-p-title">Freshwater pearl strand</h3>
      <p class="text-[1.05rem] font-bold">$120.00</p>
      <p class="text-dr-muted">Hand-knotted between every pearl, finished with a silver clasp.</p>
      <div class="mt-auto flex items-center justify-between gap-3 rounded-xl bg-dr-ink py-3 pr-3 pl-4"><p class="text-[.88rem] text-[#e2e8f0]"><strong class="text-white" data-count>2 items</strong> in your bag · <span data-subtotal>$184.00</span></p>
        <button class="cursor-pointer rounded-[10px] bg-white px-4 py-[9px] whitespace-nowrap font-bold text-dr-ink hover:bg-[#e2e8f0] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" id="dr-sheet-btn" type="button" data-drawer-open aria-controls="dr-sheet" aria-haspopup="dialog">View bag</button></div>
    </div>
  </div>
  <dialog class="fixed inset-x-0 top-auto bottom-0 mx-auto mb-0 max-h-[85dvh] w-[min(560px,100vw)] max-w-[100vw] translate-y-full rounded-t-[20px] bg-white text-dr-ink transition-[translate,display,overlay] transition-discrete duration-[320ms] ease-[cubic-bezier(.2,.8,.2,1)] motion-reduce:transition-none backdrop:bg-slate-900/50 open:flex open:translate-y-0 open:flex-col starting:open:translate-y-full" id="dr-sheet" aria-labelledby="dr-sheet-title" data-drawer>
    <div class="mx-auto mt-2.5 h-[5px] w-11 rounded-full bg-[#cbd5e1]" aria-hidden="true"></div>
    <div class="flex items-center justify-between gap-3 px-[22px] pt-[18px] pb-1.5"><h2 class="text-[1.15rem] font-bold" id="dr-sheet-title">Your bag<span class="ml-1.5 text-[.9rem] font-medium text-dr-muted" data-count>2 items</span></h2>
      <button class="grid size-[38px] cursor-pointer place-items-center rounded-full bg-dr-soft text-dr-ink hover:bg-dr-line focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-drawer-close aria-label="Close bag"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
    <div class="bg-dr-soft px-[22px] py-3.5 text-[.85rem] font-semibold mx-[22px] rounded-[10px]"><p id="dr2-free" data-free-msg>Spend $16.00 more for free shipping.</p>
      <div class="mt-2 h-1.5 overflow-hidden rounded-full bg-[#d5dce6]" role="progressbar" aria-labelledby="dr2-free" aria-valuemin="0" aria-valuemax="100" aria-valuenow="92"><span class="block h-full rounded-[inherit] bg-dr-accent transition-[width] duration-300 motion-reduce:transition-none" data-free-bar style="width:92%"></span></div></div>
    <div class="flex-1 overflow-y-auto px-[22px]" data-filled>
      <ul>
          <li class="grid grid-cols-[72px_minmax(0,1fr)_auto] gap-3.5 border-b border-dr-line py-[18px] last:border-b-0" data-line data-name="Freshwater pearl strand" data-price="120.00">
            <img class="h-[72px] w-[72px] rounded-[10px] bg-dr-soft object-cover" src="https://images.unsplash.com/photo-1515562141207-7a88fb7ce338?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="text-[.95rem] leading-[1.3] font-semibold">Freshwater pearl strand</h3><p class="mt-0.5 text-[.82rem] text-dr-muted">45 cm · Silver clasp · $120.00</p>
              <div class="mt-2.5 flex items-center gap-2"><label class="text-[.75rem] font-semibold text-dr-muted" for="dr2-q1">Qty<span class="sr-only"> of Freshwater pearl strand</span></label>
                <div class="inline-flex items-center rounded-lg border border-dr-field focus-within:border-dr-accent focus-within:shadow-[0_0_0_3px_rgba(30,58,138,.3)]"><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-dec aria-label="Decrease quantity of Freshwater pearl strand"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[30px] w-8 appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="dr2-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-inc aria-label="Increase quantity of Freshwater pearl strand"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="flex flex-col items-end justify-between"><p class="font-bold tabular-nums" data-line-total>$120.00</p><button class="cursor-pointer py-0.5 text-[.82rem] text-dr-muted underline underline-offset-[3px] hover:text-dr-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-remove>Remove<span class="sr-only"> Freshwater pearl strand</span></button></div>
          </li>
          <li class="grid grid-cols-[72px_minmax(0,1fr)_auto] gap-3.5 border-b border-dr-line py-[18px] last:border-b-0" data-line data-name="Gold hoop earrings" data-price="64.00">
            <img class="h-[72px] w-[72px] rounded-[10px] bg-dr-soft object-cover" src="https://images.unsplash.com/photo-1617038220319-276d3cfab638?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="72" height="88">
            <div><h3 class="text-[.95rem] leading-[1.3] font-semibold">Gold hoop earrings</h3><p class="mt-0.5 text-[.82rem] text-dr-muted">Small · Gold vermeil · $64.00</p>
              <div class="mt-2.5 flex items-center gap-2"><label class="text-[.75rem] font-semibold text-dr-muted" for="dr2-q2">Qty<span class="sr-only"> of Gold hoop earrings</span></label>
                <div class="inline-flex items-center rounded-lg border border-dr-field focus-within:border-dr-accent focus-within:shadow-[0_0_0_3px_rgba(30,58,138,.3)]"><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-dec aria-label="Decrease quantity of Gold hoop earrings"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[30px] w-8 appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="dr2-q2" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="grid size-[30px] cursor-pointer place-items-center rounded-[7px] text-dr-ink hover:bg-dr-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#94a3b8] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-inc aria-label="Increase quantity of Gold hoop earrings"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div></div>
            <div class="flex flex-col items-end justify-between"><p class="font-bold tabular-nums" data-line-total>$64.00</p><button class="cursor-pointer py-0.5 text-[.82rem] text-dr-muted underline underline-offset-[3px] hover:text-dr-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-remove>Remove<span class="sr-only"> Gold hoop earrings</span></button></div>
          </li>
      </ul>
    </div>
    <div class="grid flex-1 place-content-center justify-items-center gap-2 px-7 py-10 text-center" data-empty hidden>
      <div class="mb-1.5 grid size-[68px] place-items-center rounded-full bg-dr-soft text-dr-accent"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
      <h3 class="text-[1.15rem] font-bold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" tabindex="-1">Your bag is empty</h3>
      <p class="mb-2.5 text-dr-muted">Pieces you add will wait here for you.</p>
      <a class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] bg-dr-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-dr-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" href="#">Shop jewellery</a>
    </div>
    <div class="border-t border-dr-line bg-white px-[22px] pt-[18px] pb-[22px]" data-filled>
      <dl class="mb-3 grid gap-1.5">
        <div class="flex justify-between gap-3"><dt class="text-dr-muted">Subtotal</dt><dd class="font-semibold tabular-nums" data-subtotal>$184.00</dd></div>
        <div class="flex justify-between gap-3"><dt class="text-dr-muted">Shipping</dt><dd class="font-semibold tabular-nums" data-shipping>$8.00</dd></div>
        <div class="mt-1 flex justify-between gap-3 border-t border-dr-line pt-2 text-[1.08rem]"><dt class="font-bold">Total</dt><dd class="font-extrabold tabular-nums" data-total>$192.00</dd></div>
      </dl>
      <div class="grid grid-cols-2 gap-2.5"><button class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] border border-dr-accent bg-white px-[18px] py-3 font-bold text-dr-accent hover:bg-dr-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" type="button" data-drawer-close>Keep shopping</button><a class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-[10px] bg-dr-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-dr-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dr-accent" href="#">Checkout</a></div>
    </div>
    <p class="sr-only" role="status" data-announce></p>
  </dialog>
</section>
</div>
CSS
@theme {
  --color-dr-accent: #1e3a8a;
  --color-dr-accent-dark: #172c6b;
  --color-dr-ink: #0f172a;
  --color-dr-muted: #475569;
  --color-dr-line: #e2e8f0;
  --color-dr-field: #64748b;
  --color-dr-soft: #f1f5f9;
}
JavaScript
// Cart: quantity steppers, remove buttons and totals for every [data-cart] on the page. Prices come from
// data-price on each [data-line] and are summed in cents. Shipping is data-ship unless the subtotal reaches
// data-free-over, and [data-free-msg] / [data-free-bar] show how far off free shipping is. [data-badge]
// gets the item count. Changes are announced through the cart's polite [data-announce] region.
document.querySelectorAll('[data-cart]').forEach(function (cart) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var freeOver = cart.dataset.freeOver ? cents(cart.dataset.freeOver) : Infinity;
  var fee = cents(cart.dataset.ship || 0);
  var status = cart.querySelector('[data-announce]');
  function put(sel, text) { cart.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = 0, count = 0;
    cart.querySelectorAll('[data-line]').forEach(function (line) {
      var input = line.querySelector('[data-qty]'), min = +input.min || 1, max = +input.max || 99;
      var qty = Math.min(max, Math.max(min, parseInt(input.value, 10) || min));
      var total = cents(line.dataset.price) * qty;
      input.value = qty; sub += total; count += qty;
      line.querySelector('[data-line-total]').textContent = money.format(total / 100);
      line.querySelector('[data-dec]').setAttribute('aria-disabled', qty <= min);
      line.querySelector('[data-inc]').setAttribute('aria-disabled', qty >= max);
    });
    var ship = sub === 0 || sub >= freeOver ? 0 : fee;
    put('[data-subtotal]', money.format(sub / 100));
    put('[data-shipping]', ship ? money.format(ship / 100) : 'Free');
    put('[data-total]', money.format((sub + ship) / 100));
    put('[data-count]', count + (count === 1 ? ' item' : ' items'));
    put('[data-badge]', String(count));
    put('[data-free-msg]', sub >= freeOver ? 'You have unlocked free shipping.' : 'Spend ' + money.format((freeOver - sub) / 100) + ' more for free shipping.');
    cart.querySelectorAll('[data-free-bar]').forEach(function (bar) {
      var pct = Math.min(100, Math.round(sub / freeOver * 100));
      bar.style.width = pct + '%';
      bar.parentElement.setAttribute('aria-valuenow', pct);
    });
    cart.querySelectorAll('[data-filled]').forEach(function (el) { el.hidden = !count; });
    cart.querySelectorAll('[data-empty]').forEach(function (el) { el.hidden = !!count; });
    return { sub: sub, count: count };
  }
  function announce(text) { if (status) status.textContent = text; }
  cart.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-inc],[data-dec],[data-remove]');
    if (!btn) return;
    var line = btn.closest('[data-line]'), name = line.dataset.name, input = line.querySelector('[data-qty]');
    if (btn.hasAttribute('data-remove')) {
      var next = line.nextElementSibling || line.previousElementSibling;
      line.remove();
      var r = render();
      announce('Removed ' + name + '. ' + (r.count ? 'Subtotal ' + money.format(r.sub / 100) + '.' : 'Your cart is empty.'));
      var target = r.count ? next.querySelector('[data-remove]') : cart.querySelector('[data-empty] [tabindex="-1"]');
      if (target) target.focus();
      return;
    }
    if (btn.getAttribute('aria-disabled') === 'true') return;
    input.value = (parseInt(input.value, 10) || 1) + (btn.hasAttribute('data-inc') ? 1 : -1);
    var s = render();
    announce(name + ', quantity ' + input.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  cart.addEventListener('change', function (e) {
    if (!e.target.matches('[data-qty]')) return;
    var s = render();
    announce(e.target.closest('[data-line]').dataset.name + ', quantity ' + e.target.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  render();
});

// Drawer: a [data-drawer-open] button opens the <dialog> named by its aria-controls with showModal(), which
// keeps focus inside and closes on Escape. The backdrop and [data-drawer-close] buttons close it too, and focus
// goes back to the button that opened it.
document.querySelectorAll('[data-drawer-open]').forEach(function (btn) {
  var dialog = document.getElementById(btn.getAttribute('aria-controls'));
  btn.addEventListener('click', function () { dialog.showModal(); dialog.dataset.opener = btn.id; });
});
document.querySelectorAll('dialog[data-drawer]').forEach(function (dialog) {
  dialog.addEventListener('click', function (e) {
    if (e.target === dialog || e.target.closest('[data-drawer-close]')) dialog.close();
  });
  dialog.addEventListener('close', function () {
    var opener = document.getElementById(dialog.dataset.opener || '');
    if (opener) opener.focus();
  });
});

About this set

Two cart panels that open over the page instead of sending people to a separate cart. Right-side drawer opens from a Bag button with a count badge in a clothing shop header and slides in from the right with a free-shipping message and progress bar, line items with quantity steppers and remove links, a subtotal, shipping and total, a checkout button and a link to the full cart. Bottom sheet opens from a dark mini bar on a product card, rises from the bottom like a native phone sheet and ends with Keep shopping and Checkout buttons side by side. In the plain CSS and Tailwind versions both panels are a native dialog element opened with showModal(), so the browser keeps focus inside, closes on Escape and dims the page behind a backdrop; a click on the backdrop also closes it and focus returns to the button that opened it. The slide uses @starting-style with display transitions, so browsers without them simply show the panel. The Bootstrap version uses Bootstrap’s own offcanvas component in its end and bottom placements. Inside, the same cart script as the cart pages recalculates every total in cents and moves the shipping bar.

What’s included

  • Right-side drawer and bottom sheet
  • Native dialog with showModal() in CSS and Tailwind, Bootstrap offcanvas in the Bootstrap version
  • Slide-in animation with @starting-style, no animation library
  • Free-shipping progress bar that moves as quantities change
  • Header badge kept in step with the item count

Accessibility

Each panel is labelled by its heading and opens as a modal, so focus stays inside, Escape closes it and focus goes back to the opening button. The close buttons are named Close bag, quantity inputs have visible labels, and the free-shipping bar is a progressbar labelled by its message. Changes are announced in a polite status region, and the slide and progress transitions switch off under prefers-reduced-motion.

Customise it

Set --dr-accent, --dr-line and --dr-soft on .dr, and the panel width on .dr-drawer (Bootstrap: --bs-offcanvas-width). The free-shipping threshold and fee are the data-free-over and data-ship attributes on each [data-cart]. In Tailwind edit the dr-* colours in @theme and the w-[min(420px,100vw)] class.