Deck is a stylesheet, not a PHP library, so this class does three things: tell you
where the assets are, emit the tags, and version the URLs so a browser picks up a new
build. None of it is required — if you would rather write the
<link> tag yourself, write the <link> tag
yourself, and nothing else in Deck will notice.
Installing
composer require echodial/deck
PHP 8.1 or newer, and no runtime dependencies. It is framework-agnostic on purpose —
no container, no service provider, no facade — so the same class works in Keel, in
Laravel, in Symfony, in WordPress, and in a single index.php.
A browser cannot read vendor/, so the assets have to be copied into a
public directory, and Composer will not do it for you: it never runs scripts from a
package you install, only the ones in your own composer.json. Add the
publisher there:
composer require, composer install and
composer update then publish, and composer deck-publish
publishes on demand. extra.deck is read from your
composer.json, never from Deck's.
composer deck-publish
composer deck-publish -- public/static/deck
composer deck-publish -- --link # symlink where the platform allows it
publish-to defaults to public/assets/deck. Change it if your
document root is not public/: on cPanel and Helm it is
public_html/. With auto-publish left out or
false, the install and update hooks print a reminder instead of copying.
The install page covers the two ways to get
the files without any scripts.
The installer skips a file whose contents are unchanged rather than recopying it. That
is deliberate: recopying rewrites the mtime, and the mtime is what
Deck::asset() uses to bust the cache — so a churning deploy would
invalidate every asset URL on every release for no reason. Symlinks fall back to a
copy where the platform refuses them, which is Windows without developer mode and
some containers.
Configuration
Ten options, all with defaults that work. The two worth setting explicitly are
base, which nothing can guess, and root, which everything
to do with cache busting depends on.
Configuration options and their defaults
Option
Default
What it does
base
'/assets/deck'
The public URL prefix the assets are served from. Everything else is built on this.
root
null
The filesystem path that base hangs off, used to read mtimes and to check which files exist. Falls back to $_SERVER["DOCUMENT_ROOT"].
minify
null
true or false to force the choice. null decides by looking for deck.min.css on disk.
bust
true
Append ?v= to asset URLs, from the file mtime where the file can be found and from the package version otherwise.
extras
true
Emit the deck-extras.js tag.
adapters
false
Emit the deck-adapters.js tag. Off by default because it does nothing unless one of the optional libraries is on the page.
bundle
false
Emit one combined script instead of two or three tags.
defer
true
Put defer on the script tags.
hue
null
An OKLCH hue angle emitted as an inline :root style from head().
theme
null
'light' or 'dark', emitted as data-theme by htmlAttributes(). null follows the operating system.
root is the one that catches people. It is the filesystem path that
base is relative to — usually your document root, not the directory the
class is in. Get it wrong and nothing breaks visibly: the tags still render, the URLs
still work, and ?v= quietly falls back to the package version, so a
deploy stops busting caches and nobody finds out until a stale stylesheet is on a
customer's screen.
Methods
All static, all on EchoDial\Deck\Deck. Deck::VERSION is the
package version as a string.
Deck::configure(array $options): void
Merges options into the configuration. Call it once, early — before anything else on this page, since every other method reads from it.
Parameters for Deck::configure(array $options): void
Parameter
Type
Meaning
$options
array
Any of the keys in the table above. Anything you leave out keeps its default.
Returns
Nothing.
When what it needs is missing
The configuration is static, so it is per-request and shared across the whole request. Calling it twice merges rather than replaces, which means you cannot unset a key by omitting it — pass the default back explicitly. There is no validation: an unknown key is stored and ignored, so a typo is silent.
use EchoDial\Deck\Deck;
Deck::configure([
'base' => '/assets/deck',
'root' => __DIR__, // this file's directory is the web root
'extras' => true,
]);
Deck::head(bool $viewport = true): string
Everything for the <head>, in one call: the viewport meta tag Deck's mobile-first layout assumes, the stylesheet link, any theme override, the script tags, and a small inline script pointing Deck.iconSprite at the right URL.
Parameters for Deck::head(bool $viewport = true): string
Parameter
Type
Meaning
$viewport
bool
Pass false if your layout already emits the viewport meta tag. Emitting it twice is harmless but untidy.
Returns
An HTML string of several tags separated by newlines. Not escaped — it is markup, and it is meant to be echoed raw.
When what it needs is missing
The viewport tag includes viewport-fit=cover, which is what makes .safe-top and .safe-bottom do anything on a notched phone. Suppress it with false and you must include that yourself or the safe-area utilities become no-ops.
The script tags, in the right order, honouring the extras, adapters, bundle and defer options.
Returns
One to three <script> tags separated by newlines.
When what it needs is missing
With bundle on, it looks for deck.bundle.min.js on disk and falls back to the unminified bundle when it is not there — so a project that only published the readable build still gets a working tag rather than a 404. That check needs root to be right.
The public URL for one file under base, with a cache-busting query where one can be worked out. This is what everything else here calls.
Parameters for Deck::asset(string $file): string
Parameter
Type
Meaning
$file
string
A filename such as 'deck-icons.svg'.
Returns
The URL, with ?v= appended unless bust is off.
When what it needs is missing
The version comes from the file's mtime, which requires finding it on disk: root if you set it, otherwise $_SERVER["DOCUMENT_ROOT"]. On a CLI request, or behind a server that does not set that, or where the path does not resolve, it falls back to the package version instead — so the URL is still stable and still works, but a deploy that does not change the version will not bust the cache. If cache busting matters, set root explicitly.
With minify at its default of null, it checks whether deck.min.css exists under root. Where root cannot be worked out, that check fails and it returns the unminified name — correct, but larger than you wanted. Set minify to true in production and the check is skipped.
Deck::stylesheet(); // 'deck.min.css' in production
A space-separated attribute string with no leading space, ready to drop straight into the tag. Empty when nothing is configured.
When what it needs is missing
Only emits data-theme when the theme option is not null. Leaving it null is usually right: the operating system decides, and the script restores a saved choice before first paint.
The per-tenant theming this whole framework was built for: one inline style on the <html> tag, no rebuild, no second stylesheet, and the right colours in the very first paint.
An OKLCH hue angle, 0 to 360, emitted as --hue-brand.
$mode
?string
'light' or 'dark', emitted as data-theme.
$chroma
?float
Emitted as --chroma-brand. Lower is more muted. Trailing zeros are trimmed.
Returns
A style attribute, a data-theme attribute, both, or an empty string. Values are escaped.
When what it needs is missing
Every argument is optional and a null one is simply omitted, so a tenant with no brand colour and a user with no theme preference produce an empty string and the defaults apply. Because it is server-rendered there is no flash of the wrong brand colour, which is the reason to prefer it over Deck.hue() in JavaScript.
Classes for the svg. icon-sm, icon-lg and icon-fill are the usual additions.
$label
?string
An accessible name. Omit it for a decorative icon.
Returns
An SVG element as a string. The name, the class and the sprite URL are all escaped.
When what it needs is missing
With no $label the icon gets aria-hidden="true", which is right when there is text beside it and wrong when the icon is the only content — an icon-only button with no label is announced as "button". Pass $label there, or put the words in a .sr-only span. A name that is not in the sprite renders an empty svg rather than an error, so check the spelling if an icon simply does not appear.
Parameters for Deck::config(string $key, mixed $default = null): mixed
Parameter
Type
Meaning
$key
string
The option name.
$default
mixed
Returned when the key was never set.
Returns
The value, or the default.
When what it needs is missing
A key that was never set and has no default returns null, which is indistinguishable from an option genuinely set to null — hue and theme both default to null. Pass a sentinel default if you need to tell those apart.
The absolute filesystem path to Deck's own dist directory inside vendor/. For a build step or a deploy script that wants to copy the files somewhere itself.
Returns
An absolute path.
When what it needs is missing
It is the path inside the installed package, so a browser cannot read it — the files still have to be published into a public directory. That is what the Composer installer below does.
A complete page. The tenant's brand colour and the user's theme come out of the
database and land in the first byte of HTML, so there is no flash of the wrong colour
and no second request.
There is a runnable version of this in the repository at
public_html/php-helper.php. It deliberately requires the class by path
rather than through the autoloader, so it works in a bare PHP site with nothing
installed.
Things worth knowing
Everything returns a string. Nothing echoes, nothing buffers, and
nothing touches global state beyond the static configuration. The methods are safe
to call in a template.
Markup is not escaped; values are. The return values are HTML and
are meant to be echoed raw. The parts that come from you — the icon name, the
class, the theme, the URLs — are escaped inside.
The configuration is static. Per request, shared across the
request. In a long-running worker — Swoole, RoadRunner, Octane — it survives
between requests, so configure it once at boot rather than per request, or reset it
explicitly.
It does not read the stylesheet. Nothing here parses CSS or knows
what classes exist. If you want that inventory in PHP, read
dist/api.json — the documentation site does exactly that.