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.
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.
| Route | The work | What 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.
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
| File | What it is |
|---|---|
deck.min.css | The stylesheet. This is the one to link. |
deck.css | The same stylesheet, unminified, for reading. |
deck.js | The core script. Which components need it. |
deck-extras.js | Carousel, drawer, mega menu, copy button, QR codes and the editor. Load it after deck.js. |
deck-adapters.js | Only for use alongside Floating UI, Chart.js, SortableJS and similar libraries. Load it after deck.js. |
deck.bundle.min.js | All three scripts in one file, minified. |
deck-icons.svg | The 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
| Import | File | Use it for |
|---|---|---|
@echodial/deck/css | dist/deck.css | The stylesheet. |
@echodial/deck/css/min | dist/deck.min.css | The stylesheet, already minified. |
@echodial/deck/icons | dist/deck-icons.svg | The sprite, imported for its URL. See below. |
@echodial/deck/js | dist/deck.js | The core script on its own. |
@echodial/deck/extras | dist/deck-extras.js | The extended components. Import it after /js. |
@echodial/deck/adapters | dist/deck-adapters.js | Third-party library adapters. Import it after /js. |
@echodial/deck/bundle | dist/deck.bundle.js | All three scripts. Sets window.Deck. |
@echodial/deck/layers/* | dist/layers/*.css | One cascade layer at a time, such as /layers/tokens.css. |
@echodial/deck | dist/deck.esm.js | Nothing, 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.
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
| Flag | What lands |
|---|---|
| none | deck.css, deck-icons.svg, deck.js, deck-extras.js, deck-adapters.js |
--min | deck.min.css in place of deck.css |
--bundle | deck.bundle.min.js in place of the three scripts |
--css-only | deck.css and deck-icons.svg, no scripts |
--force | The 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/deckon cPanel, Plesk and Helm,public/assets/deckon Laravel and Symfony. Left out, it ispublic/assets/deck, which is wrong for everypublic_html/site. auto-publish-
true: everycomposer installandcomposer updatecopies the files, skipping any that have not changed.falseor left out: those commands print a one-line reminder instead, andcomposer deck-publishcopies 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
- The stylesheet loaded. The button is a filled, rounded, brand-coloured rectangle, not a grey system button.
- An icon rendered. A check mark in a circle sits beside the badge, not an empty gap.
- 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, andDeck.hue()sets it. - 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:
Deck is installed
If this line is muted grey and the button below is a filled, rounded, brand-coloured rectangle, the stylesheet loaded.
<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:
| How the sprite was referenced | What drew |
|---|---|
<use href> to the jsDelivr URL | Nothing. Chrome logs Unsafe attempt to load URL … Domains, protocols and ports must match. |
<use href> to the unpkg URL | Nothing, with the same message. |
Deck.iconSprite as the bundle works it out when it is loaded from jsDelivr | Nothing: it points at jsDelivr. |
| The same file saved beside the page | The icon. |
The jsDelivr file fetched with fetch() and inserted into the page | The 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
| File | What needs it |
|---|---|
deck.js | Toasts, 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.js | The carousel, the drawer, the mega menu, the copy button, QR codes and the rich-text editor. Load it after deck.js. |
deck-adapters.js | Nothing 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.js | All 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.
| File | For |
|---|---|
deck-mark.svg | The favicon: the mark alone, with its own light and dark colours. There is no favicon.svg; this is it. |
deck-apple-touch-icon.png | 180 × 180, for rel="apple-touch-icon". |
deck-logo.svg, deck-logo-stacked.svg | The lockup, horizontal and stacked, drawn in currentColor. |
deck-logo-light.svg, deck-logo-dark.svg | The lockup with a fixed fill, for light and for dark backgrounds. |
deck-og.png | A 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, ordeck.min.csson 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
hrefsays. 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
Deckinto the console: if it saysundefined, 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/bundleand usewindow.Deck. - webpack says "./icons?url" is not exported, or that 'import' and 'export' may appear only with 'sourceType: module'
-
Drop the
?urland let theasset/resourcerule 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.