Install Deck

Installing with npm or Composer puts Deck in node_modules or vendor, which a browser cannot reach. Use the CDN and there is nothing to install, though icons still need one file saved to your own site. Use a bundler and there is nothing to copy. Everything else has one copy step.

public_html/ or public/

Wherever a folder appears, this page gives both names: public_html/ for cPanel, Plesk and Helm, and public/ for Laravel and Symfony. Use the one your web server serves, and run every command on this page from the folder that contains it, your project root. Any other name, such as web/, works the same way.

Pick a route

In order of how little work each one is. All four end with the same check page, so you know when you are done.

The four install routes, the work each needs, and what each costs
RouteThe workWhat it costs
1. CDN Two tags, and the icon sprite saved to your site if you use icons. Every page load asks a third party for Deck, and the version is written in your HTML rather than pinned in your repository.
2. Bundler npm install and three imports. Nothing to copy. A build step, which is the thing Deck exists so you can do without.
3. npx One command that copies five files. You run it again after every upgrade, and no lockfile records which version you copied.
4. Composer Three scripts added to your composer.json, then composer require. Scripts in your own composer.json that you maintain, and remove if you remove Deck.

1. CDN

Nothing to install. jsDelivr and unpkg serve every version published to npm, so these two tags in your page's <head> are the whole stylesheet and all of the JavaScript:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.min.css">
<script src="https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.bundle.min.js" data-deck-icons="/assets/deck/deck-icons.svg" defer></script>

What it costs. Every page load makes a request to a third party, and the version your site runs is written in your HTML rather than pinned in a lockfile in your repository.

Every URL is pinned to @0.1, never @latest: @0.1 picks up each 0.1.x fix and never moves to 0.2, while @latest, or no version at all, moves your page onto a breaking release the day it is published.

Icons will not draw from the CDN

Browsers refuse an SVG <use> whose href is on another origin, so every icon pointed at jsDelivr or unpkg is an empty box. That includes the icons Deck's own components draw, because the bundle looks for the sprite beside itself, on the CDN. Save the sprite to your own site and point data-deck-icons at it, as the tag above does. What was tested.

# public_html/ (cPanel, Plesk, Helm)
curl -sSL --create-dirs -o public_html/assets/deck/deck-icons.svg https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck-icons.svg

# public/ (Laravel, Symfony)
curl -sSL --create-dirs -o public/assets/deck/deck-icons.svg https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck-icons.svg

It prints nothing and exits with 0. That sprite is the only file this route puts on disk.

Every file

The files Deck publishes to the CDNs
FileWhat it is
deck.min.cssThe stylesheet. This is the one to link.
deck.cssThe same stylesheet, unminified, for reading.
deck.jsThe core script. Which components need it.
deck-extras.jsCarousel, drawer, mega menu, copy button, QR codes and the editor. Load it after deck.js.
deck-adapters.jsOnly for use alongside Floating UI, Chart.js, SortableJS and similar libraries. Load it after deck.js.
deck.bundle.min.jsAll three scripts in one file, minified.
deck-icons.svgThe icon sprite. Served, but it will not draw from here.

jsDelivr:

https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.min.css
https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.css
https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.js
https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck-extras.js
https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck-adapters.js
https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.bundle.min.js
https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck-icons.svg

unpkg, which answers @0.1 with a redirect to the exact release, currently @0.1.2:

https://unpkg.com/@echodial/deck@0.1/dist/deck.min.css
https://unpkg.com/@echodial/deck@0.1/dist/deck.css
https://unpkg.com/@echodial/deck@0.1/dist/deck.js
https://unpkg.com/@echodial/deck@0.1/dist/deck-extras.js
https://unpkg.com/@echodial/deck@0.1/dist/deck-adapters.js
https://unpkg.com/@echodial/deck@0.1/dist/deck.bundle.min.js
https://unpkg.com/@echodial/deck@0.1/dist/deck-icons.svg

Check it

Take the check page, replace its three asset lines, the stylesheet and the two scripts, with the two tags at the top of this section, and open it.

2. Bundler

Nothing to copy: your bundler reads Deck out of node_modules and emits what the browser needs. In your project:

npm install @echodial/deck
$ npm install @echodial/deck

added 1 package, and audited 2 packages in 2s

found 0 vulnerabilities

What it costs. A build step, which is the thing Deck exists so you can do without. This route is for a project that already has one.

Every import

The subpaths in the package's exports map
ImportFileUse it for
@echodial/deck/cssdist/deck.cssThe stylesheet.
@echodial/deck/css/mindist/deck.min.cssThe stylesheet, already minified.
@echodial/deck/iconsdist/deck-icons.svgThe sprite, imported for its URL. See below.
@echodial/deck/jsdist/deck.jsThe core script on its own.
@echodial/deck/extrasdist/deck-extras.jsThe extended components. Import it after /js.
@echodial/deck/adaptersdist/deck-adapters.jsThird-party library adapters. Import it after /js.
@echodial/deck/bundledist/deck.bundle.jsAll three scripts. Sets window.Deck.
@echodial/deck/layers/*dist/layers/*.cssOne cascade layer at a time, such as /layers/tokens.css.
@echodial/deckdist/deck.esm.jsNothing, in 0.1.2. See the warning below.

Every one of these resolved through the exports map in a production build with Vite 8.3 and with webpack 5.110.

In 0.1.2, import Deck from '@echodial/deck' does not build

The module it points at ends with an export that is not valid JavaScript, so Node, Vite and webpack all stop on it; webpack says Module parse failed: Export 'globalThis' is not defined. Import the bundle for its side effect instead, as below. It sets window.Deck, which is also what inline handlers such as onclick="Deck.toast(…)" call.

Vite

main.js, in your project root beside package.json:

import '@echodial/deck/css';
import '@echodial/deck/bundle';
import sprite from '@echodial/deck/icons?url';

const { Deck } = window;

// <use href> needs the sprite's real URL, which exists only once the build has
// emitted the file, so fill it in here: for Deck's own components, and for
// every icon in your markup that names just a symbol.
Deck.iconSprite = sprite;
for (const use of document.querySelectorAll('use[href^="#"]')) {
  use.setAttribute('href', sprite + use.getAttribute('href'));
}

An import is not a URL, and <use href> needs one. ?url asks Vite for the address of the file it emits. The loop puts that address in front of every icon in your markup written as just #name, and Deck.iconSprite does the same for the icons Deck's own components draw.

index.html, in the same folder, is the check page with one module script in place of its three asset lines, and its icon written as #check-circle:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Deck is installed</title>
<script type="module" src="/main.js"></script>
</head>
<body>

<main class="container section stack stack-6">
  <h1>Deck is installed</h1>
  <p class="lede">If this line is muted grey and the button below is a filled,
    rounded, brand-coloured rectangle, the stylesheet loaded.</p>

  <div class="cluster">
    <button class="btn btn-primary">A button</button>
    <span class="badge badge-good">A badge</span>
    <svg class="icon icon-lg" style="color:var(--brand)">
      <use href="#check-circle"></use>
    </svg>
  </div>

  <input class="range" type="range" min="0" max="360" value="196"
    aria-label="Brand hue" oninput="Deck.hue(this.value)">

  <button class="btn" onclick="Deck.toast({kind:'good', title:'JavaScript works too'})">
    Fire a toast
  </button>
</main>

</body>
</html>
npm install --save-dev vite
npx vite build
npx vite preview

npx vite build printed:

$ npx vite build
vite v8.3.0 building client environment for production...
transforming...
✓ 7 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                        1.08 kB │ gzip:  0.62 kB
dist/assets/deck-icons-zJMcLAdo.svg  110.03 kB │ gzip: 34.47 kB
dist/assets/index-BRXtUJ3R.css       172.36 kB │ gzip: 32.91 kB
dist/assets/index-61EmezT8.js         57.89 kB │ gzip: 18.23 kB

✓ built in 236ms

Vite 8 also prints a warning for each of Deck's carousel rules that use ::scroll-marker and ::scroll-button, beginning [lightningcss minify] 'scroll-marker-group' is not recognized as a valid pseudo-element. They are left out of the output above. The rules themselves are not lost: the built stylesheet has every one of them, the same number as deck.min.css.

webpack

npm install --save-dev webpack webpack-cli css-loader style-loader html-webpack-plugin

webpack.config.js:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/main.mjs',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /deck-icons\.svg$/, type: 'asset/resource' },
    ],
  },
  plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};

src/main.mjs is the Vite file with the sprite imported without ?url. webpack reads a query on a package import as part of the export's name and stops with "./icons?url" is not exported; the asset/resource rule above is what makes the plain import a URL.

import '@echodial/deck/css';
import '@echodial/deck/bundle';
import sprite from '@echodial/deck/icons';

const { Deck } = window;

// <use href> needs the sprite's real URL, which exists only once the build has
// emitted the file, so fill it in here: for Deck's own components, and for
// every icon in your markup that names just a symbol.
Deck.iconSprite = sprite;
for (const use of document.querySelectorAll('use[href^="#"]')) {
  use.setAttribute('href', sprite + use.getAttribute('href'));
}

src/index.html is the Vite index.html without its script tag, which the HTML plugin adds. Then npx webpack builds into dist/, which is the folder to serve. It warns that main.js is over its 244 KiB recommendation, because style-loader carries the stylesheet inside the script.

The entry is .mjs because npm init -y writes "type": "commonjs" into package.json, and webpack then refuses import in a .js file: 'import' and 'export' may appear only with 'sourceType: module'.

Check it

Open the address npx vite preview prints, or serve webpack's dist/, and look for the four signs.

3. npx

One command copies Deck out of npm and into your site, and installs nothing in your project:

# public_html/ (cPanel, Plesk, Helm)
npx @echodial/deck@0.1 init public_html/assets/deck

# public/ (Laravel, Symfony)
npx @echodial/deck@0.1 init public/assets/deck

What it costs. You run it again after every upgrade, and no lockfile records which version you copied; the version is on the second line of deck.css.

@0.1 is there for the same reason the CDN URLs carry it. From an empty directory, with nothing installed and an empty npm cache:

$ npx @echodial/deck@0.1 init public_html/assets/deck
npm warn exec The following package was not found and will be installed: @echodial/deck@0.1.2
  + public_html\assets\deck\deck.css
  + public_html\assets\deck\deck-icons.svg
  + public_html\assets\deck\deck.js
  + public_html\assets\deck\deck-extras.js
  + public_html\assets\deck\deck-adapters.js

Add to your layout:

  <link rel="stylesheet" href="/assets/deck/deck.css">
  <script src="/assets/deck/deck.js" defer></script>

deck-icons.svg sits beside deck.js and is found automatically.
To load the sprite from somewhere else:

  <script src="/assets/deck/deck.js" data-deck-icons="/your/path/deck-icons.svg" defer></script>

Three things in that output are worth knowing before they confuse you. In an interactive terminal npx asks before it downloads the package; answer y. The paths use backslashes because this run was on Windows. And the tags start at /assets, because init takes public_html/ or public/ to be your document root, which is not part of a URL.

Flags

What init copies with each flag
FlagWhat lands
nonedeck.css, deck-icons.svg, deck.js, deck-extras.js, deck-adapters.js
--mindeck.min.css in place of deck.css
--bundledeck.bundle.min.js in place of the three scripts
--css-onlydeck.css and deck-icons.svg, no scripts
--forceThe same files, overwriting any already there. Without it a second run copies nothing and prints 5 already there (use --force to overwrite).

Flags combine: --min --bundle copies deck.min.css, deck-icons.svg and deck.bundle.min.js. With --bundle or --css-only, the last lines init prints still say the sprite sits beside deck.js. Ignore them: the sprite is beside whichever files were copied.

What does not land

The minified single scripts (deck.min.js, deck-extras.min.js, deck-adapters.min.js), the unminified deck.bundle.js, the module entry deck.esm.js, the per-layer stylesheets in layers/, the logo and favicon files, and the build data such as api.json. A page needs none of them. npx @echodial/deck@0.1 list lists what is in the package's dist/ folder, with sizes.

Check it

The check page works unchanged.

4. Composer

composer require echodial/deck puts Deck in vendor/, which a browser cannot reach, and copies nothing out of it on its own. Deck ships a class that does the copy, EchoDial\Deck\Installer. But Composer runs scripts only from the root composer.json, the project you run composer in, and never from a package you install. A script in Deck's own composer.json would never fire in your project, so the call has to live in yours.

What it costs. Three scripts in your own composer.json that you maintain, and remove if you remove Deck.

Add these two blocks to your composer.json before you require Deck:

"scripts": {
  "post-install-cmd": ["EchoDial\\Deck\\Installer::postInstall"],
  "post-update-cmd": ["EchoDial\\Deck\\Installer::postInstall"],
  "deck-publish": "EchoDial\\Deck\\Installer::publish"
},
"extra": {
  "deck": {
    "publish-to": "public_html/assets/deck",
    "auto-publish": true
  }
}
publish-to
Where the files go, relative to composer.json. Make it your document root plus /assets/deck: public_html/assets/deck on cPanel, Plesk and Helm, public/assets/deck on Laravel and Symfony. Left out, it is public/assets/deck, which is wrong for every public_html/ site.
auto-publish
true: every composer install and composer update copies the files, skipping any that have not changed. false or left out: those commands print a one-line reminder instead, and composer deck-publish copies on demand.

Then:

composer require echodial/deck

From an empty directory holding only those two blocks, with an empty Composer cache:

$ composer require echodial/deck
./composer.json has been updated
Running composer update echodial/deck
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking echodial/deck (v0.1.2)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Downloading echodial/deck (v0.1.2)
  - Installing echodial/deck (v0.1.2): Extracting archive
Generating autoload files
> EchoDial\Deck\Installer::postInstall
Deck 0.1.1 published to public_html/assets/deck (8 copied, 0 unchanged)
  Add to your layout:
    <link rel="stylesheet" href="/assets/deck/deck.css">
    <script src="/assets/deck/deck.js" defer></script>
No security vulnerability advisories found.
Using version ^0.1.2 for echodial/deck

Why it says 0.1.1. The 0.1.2 package on Packagist was built from a commit made before its version number changed, so it calls itself 0.1.1 in that line and in its file banners. It is the package this page was tested against.

It copies eight files: deck.css, deck.min.css, deck-icons.svg, deck.js, deck-extras.js, deck-adapters.js, deck.bundle.js and deck.bundle.min.js. If Deck was already installed when you added the scripts, run composer deck-publish once. If you remove Deck, remove the scripts too: Composer keeps calling them, and without the class every install prints Class EchoDial\Deck\Installer is not autoloadable, can not call post-install-cmd script.

To keep scripts out of composer.json, use npx instead, or copy the files by hand as described below.

Check it

The check page works unchanged.

The check page

Every route ends here. Save this as public_html/deck-check.html, or public/deck-check.html, beside the assets folder. It has a name of its own so it cannot replace a home page you already have. It links the files npx and Composer copy; the CDN and bundler routes above say what to change.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Deck is installed</title>
<link rel="stylesheet" href="/assets/deck/deck.css">
<script src="/assets/deck/deck.js" defer></script>
<script src="/assets/deck/deck-extras.js" defer></script>
</head>
<body>

<main class="container section stack stack-6">
  <h1>Deck is installed</h1>
  <p class="lede">If this line is muted grey and the button below is a filled,
    rounded, brand-coloured rectangle, the stylesheet loaded.</p>

  <div class="cluster">
    <button class="btn btn-primary">A button</button>
    <span class="badge badge-good">A badge</span>
    <svg class="icon icon-lg" style="color:var(--brand)">
      <use href="/assets/deck/deck-icons.svg#check-circle"></use>
    </svg>
  </div>

  <input class="range" type="range" min="0" max="360" value="196"
    aria-label="Brand hue" oninput="Deck.hue(this.value)">

  <button class="btn" onclick="Deck.toast({kind:'good', title:'JavaScript works too'})">
    Fire a toast
  </button>
</main>

</body>
</html>

Serve the folder rather than opening the file: the paths start at the site root, and a file:// address has no site root for them to start from.

# public_html/ (cPanel, Plesk, Helm)
php -S localhost:8000 -t public_html
python -m http.server 8000 --directory public_html

# public/ (Laravel, Symfony)
php -S localhost:8000 -t public
python -m http.server 8000 --directory public

Then open http://localhost:8000/deck-check.html, and delete the file once all four signs below are there.

What success looks like

  1. The stylesheet loaded. The button is a filled, rounded, brand-coloured rectangle, not a grey system button.
  2. An icon rendered. A check mark in a circle sits beside the badge, not an empty gap.
  3. The hue slider retunes. Drag it, and the button and the icon change colour together, because every brand colour comes from one custom property, --hue-brand, and Deck.hue() sets it.
  4. A toast fires. Press Fire a toast, and a notification saying JavaScript works too slides in from the corner.

Rendered here by the same stylesheet, without the toast button. The slider retunes this page too:

The check page, rendered by the stylesheet this site uses

Deck is installed

If this line is muted grey and the button below is a filled, rounded, brand-coloured rectangle, the stylesheet loaded.

A badge
<div class="stack stack-4"> <h3>Deck is installed</h3> <p class="lede">If this line is muted grey and the button below is a filled, rounded, brand-coloured rectangle, the stylesheet loaded.</p> <div class="cluster"> <button class="btn btn-primary">A button</button> <span class="badge badge-good">A badge</span> <svg class="icon icon-lg" style="color:var(--brand)"><use href="../../assets/deck/deck-icons.svg#check-circle"></use></svg> </div> <input class="range" type="range" min="0" max="360" value="196" aria-label="Brand hue" oninput="Deck.hue(this.value)"> </div>
<div class="stack stack-4">
  <h3>Deck is installed</h3>
  <p class="lede">If this line is muted grey and the button below is a filled, rounded, brand-coloured rectangle, the stylesheet loaded.</p>
  <div class="cluster">
    <button class="btn btn-primary">A button</button>
    <span class="badge badge-good">A badge</span>
    <svg class="icon icon-lg" style="color:var(--brand)"><use href="../../assets/deck/deck-icons.svg#check-circle"></use></svg>
  </div>
  <input class="range" type="range" min="0" max="360" value="196" aria-label="Brand hue" oninput="Deck.hue(this.value)">
</div>

viewport-fit=cover is not decoration. It is what lets .safe-top and .safe-bottom reach around the notch and the home indicator on a phone. Leave it out and those become no-ops.

Assets

The stylesheet is not all Deck ships. This is where each of the other pieces comes from.

Icons

The icon sprite, deck-icons.svg, is a file of its own, separate from the stylesheet, and it has to be served from the same origin as your page. An SVG <use> that points at another origin is blocked by the browser, so the copy on a CDN will not draw. Tested in Chrome, from a page served on 127.0.0.1:

Where the sprite was loaded from and what drew
How the sprite was referencedWhat drew
<use href> to the jsDelivr URLNothing. Chrome logs Unsafe attempt to load URL … Domains, protocols and ports must match.
<use href> to the unpkg URLNothing, with the same message.
Deck.iconSprite as the bundle works it out when it is loaded from jsDelivrNothing: it points at jsDelivr.
The same file saved beside the pageThe icon.
The jsDelivr file fetched with fetch() and inserted into the pageThe icon.

Two settings tell Deck where the sprite is. On the script tag, data-deck-icons, read once as the script loads. In code, Deck.iconSprite, which applies to every icon Deck draws after you set it:

<script src="/assets/deck/deck.js" data-deck-icons="/assets/deck/deck-icons.svg" defer></script>

Deck.iconSprite = '/assets/deck/deck-icons.svg';

With neither, deck.js looks for the sprite beside its own URL, which is why the npx and Composer routes need no setting. Neither setting touches your own markup: an icon you write names the file itself, as <svg class="icon"><use href="/assets/deck/deck-icons.svg#name"></use></svg>.

If the sprite really has to come from a CDN, fetch it and put it in the page, then write icons as <use href="#name">, which refers to the copy in the document. Until the fetch finishes every icon is blank, and each page carries the whole 110 KB sprite in its markup.

fetch('https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck-icons.svg')
  .then((r) => r.text())
  .then((svg) => {
    const holder = document.createElement('div');
    holder.style.cssText = 'position:absolute;inline-size:0;block-size:0;overflow:hidden';
    holder.innerHTML = svg;
    document.body.prepend(holder);
  });

Icon names, sizes and the two weights are on the icon page.

Emoji

Nothing to install. An emoji is a text character, drawn by the emoji font already on the reader's device, so there is no emoji file in Deck and none to download. What Deck supplies is .emoji, which names the colour emoji font stack and fixes the size and baseline so the character sits on the line with its text. The emoji page has the sizes and tiles.

JavaScript

Which components need which script
FileWhat needs it
deck.jsToasts, the theme switch, Deck.hue(), the date picker, the combobox, the data grid, count-up numbers, flip cards, back-to-top, and pairing each popover with the button that opens it so the browser's anchor positioning can place it.
deck-extras.jsThe carousel, the drawer, the mega menu, the copy button, QR codes and the rich-text editor. Load it after deck.js.
deck-adapters.jsNothing on its own. When Floating UI, Quill, Tiptap, Chart.js, SortableJS or Lucide is already on the page, it hands that job to the library. Load it after deck.js.
deck.bundle.min.jsAll three, in that order, in one file.

Everything else is CSS and needs no script at all: the layout, buttons, forms, tables, cards, the dialog-based overlays, <details> accordions and tabs. The JavaScript reference lists every attribute and method.

Brand and favicon

Deck's logo files are in dist/brand/ of the package. No route copies them; take the ones you use.

The brand files in the package
FileFor
deck-mark.svgThe favicon: the mark alone, with its own light and dark colours. There is no favicon.svg; this is it.
deck-apple-touch-icon.png180 × 180, for rel="apple-touch-icon".
deck-logo.svg, deck-logo-stacked.svgThe lockup, horizontal and stacked, drawn in currentColor.
deck-logo-light.svg, deck-logo-dark.svgThe lockup with a fixed fill, for light and for dark backgrounds.
deck-og.pngA 1200 × 630 social card.
node_modules/@echodial/deck/dist/brand/     npm and bundlers
vendor/echodial/deck/dist/brand/            Composer
https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/brand/deck-mark.svg

Put the favicon beside Deck's other files and link it:

<link rel="icon" href="/assets/deck/deck-mark.svg" type="image/svg+xml">

For a logo that follows --hue-brand, use the sprite instead of a file. A logo in an <img> or a <link> has no colour context, so its currentColor comes out black, which is why the fixed-fill files exist. The marks are filled rather than stroked, hence .icon-fill:

<svg class="icon icon-lg icon-fill" style="color:var(--brand)">
  <use href="/assets/deck/deck-icons.svg#deck-mark"></use>
</svg>

Without Node or Composer

Download or copy the files by hand

Only if none of the four routes fits. Download the four files the check page loads:

# public_html on cPanel, Plesk and Helm; public on Laravel and Symfony
root=public_html
curl -sSL --create-dirs -o $root/assets/deck/deck.css https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.css
curl -sSL --create-dirs -o $root/assets/deck/deck-icons.svg https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck-icons.svg
curl -sSL --create-dirs -o $root/assets/deck/deck.js https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck.js
curl -sSL --create-dirs -o $root/assets/deck/deck-extras.js https://cdn.jsdelivr.net/npm/@echodial/deck@0.1/dist/deck-extras.js

Or, with Deck already in vendor/ and no scripts in composer.json, copy the same four out of it:

# public_html on cPanel, Plesk and Helm; public on Laravel and Symfony
root=public_html
mkdir -p $root/assets/deck
for f in deck.css deck-icons.svg deck.js deck-extras.js; do cp "vendor/echodial/deck/dist/$f" "$root/assets/deck/"; done

Set root to the folder your web server serves first. Both are for a POSIX shell: Git Bash on Windows, macOS, or Linux. The check page works unchanged afterwards. Repeat the download or the copy after every upgrade.

If something did not work

The page is unstyled: plain black Times New Roman
The stylesheet did not load. Open the network tab and find the request for deck.css, or deck.min.css on the CDN. A 404 there is nearly always a path: the page links /assets/… from the site root, and the site is served from a subfolder, or the files went to the wrong folder.
Everything is styled but every icon is an empty box
The sprite is not reachable from the page's own origin. Either it points at a CDN, which never works, or the file is not where the href says. Open the sprite's URL in the browser before you check anything else.
The button looks right but the toast and the slider do nothing
The script is missing or failed to parse. Type Deck into the console: if it says undefined, the script never ran. If you see a warning reading load deck.js first, the script tags are in the wrong order.
A bundler stops with Export 'globalThis' is not defined
You wrote import Deck from '@echodial/deck', which does not build in 0.1.2. Import @echodial/deck/bundle and use window.Deck.
webpack says "./icons?url" is not exported, or that 'import' and 'export' may appear only with 'sourceType: module'
Drop the ?url and let the asset/resource rule make the import a URL. For the second message, name the entry .mjs. Both are shown above.
Composer says Command "deck-publish" is not defined
The scripts are not in your composer.json. Composer never runs the ones inside a package. Add the two blocks.
The page is dark and you expected light
That is correct behaviour: Deck follows the operating system's colour scheme with no configuration. Add a theme switch if you want a person to be able to override it.
It looks right but everything is a bit too big or too small
Deck sizes in rem, so it follows the browser's font size on purpose. That is a reader's setting, and Deck respects it rather than overriding it.

Next

You have a page that loads Deck. The next tutorial builds something real with it: a full account settings page, typed line by line, in one sitting. If you write in Visual Studio Code, Cursor or Antigravity, take two minutes to install Deck IntelliSense first, so the editor completes class names and catches the typos for you.