Everything you need to know to use the dotHello v1.8.1 framework in your own project.
dotHello is a lightweight CSS and JS framework for dark-mode-first microsites — landing pages, digital business cards, product pages, and small web apps. It gives you a set of well-designed components out of the box, driven entirely by CSS custom properties so you can brand it in minutes.
<link> tags into any HTML file.data-dh-* attributes.prefers-reduced-motion.Copy the css/ and js/ folders into your project, then add to your HTML <head> and just before </body>:
<link rel="stylesheet" href="css/dothello.css">
<link rel="stylesheet" href="css/tokens.css">
<script src="js/dothello.js"></script>
<script>DH.init();</script>
<link rel="stylesheet" href="https://hellodjt.com/dh/dothello.css">
<link rel="stylesheet" href="https://hellodjt.com/dh/tokens.css">
<!-- before </body> -->
<script src="https://hellodjt.com/dh/dothello.js"></script>
<script>DH.init();</script>
DH.init() calls every init* function at once. Place the <script> tag at the end of <body> (not with defer) so the DOM is fully parsed before DH.init() runs. Open demo/index.html to see every component working live.DH object with 28 functions covering scroll reveal, dark mode, modals, tabs, accordions, countdown timers, toast notifications, lightbox, audio player, form validation, dropdowns, progress bars, skeleton loaders, video embeds, parallax, Lottie animations, smooth scroll, RTL support, and panel transitions. Call DH.init() once to activate all features automatically.Open css/tokens.css and change the values in the :root block. The most important token is --dh-color-primary — update it to your brand colour and buttons, links, and highlights all update automatically.
:root {
--dh-color-primary: #your-colour; /* brand accent */
--dh-font-body: 'Your Font', sans-serif;
--dh-color-dark: #your-dark-bg; /* page background */
}
The kit's tokens.css has every token listed with comments explaining what it does. The key tokens are:
| Token | Default in kit | Use |
|---|---|---|
| --dh-color-primary | #f59e0b | Buttons, links, highlights, active states |
| --dh-color-dark | #0f0f0f | Page background (dark) |
| --dh-color-dark-soft | #1a1a1a | Alt section backgrounds |
| --dh-color-dark-mid | #222 | Card and panel backgrounds |
| --dh-color-text-inverse | #f0f0f0 | Text on dark surfaces |
| --dh-color-text-muted | #6b7280 | Secondary / hint text |
| --dh-font-body | Inter, system-ui | Body typeface |
| --dh-shadow-glow | amber radial glow | Hero glow — update to match primary |
| --dh-radius | 10px | Card and box border-radius |
dotHello is dark-mode first. The framework and tokens.css set a dark background and light text by default. For a light-background page (like this guide), add a page-level override in your own <style> block:
/* Override dark-mode tokens for this page */
:root {
--dh-color-text: #1c1c1c;
--dh-color-text-muted: #6b7280;
}
body { background: #f5f5f5; color: #1c1c1c; }
color value on light pages — do not use var(--dh-color-text) there, because the token resolves to the dark-mode value from tokens.css before your override takes effect.Components like dh-nav and dh-app-header use var(--dh-color-dark) internally, so they keep their dark appearance even on a light-background page — which is usually the desired result.
All components work by adding class names to standard HTML elements. Open demo/index.html to see them all rendered live. Below is the complete class reference.
.is-scrolled class automatically on scroll--dh-shadow-glow.is-active.is-active to be shown; all others are hidden<html> or <body> — enables dark mode overrides<img> elements inside a flex rowDH.initLightbox() — no extra markup needed on the container itselfInclude js/dothello.js at the end of <body> and call DH.init(). Every behaviour below is activated automatically when you call DH.init(). You can also call individual init* functions yourself if you only need specific features.
DH.init() is a single convenience call that invokes all init* functions at once. Call it once at the end of <body> after your markup.
DH.init(); // activates all 28 behaviours in one call
// Or call individual functions:
DH.initScrollReveal();
DH.initModals();
DH.initTabs();
Add data-dh-reveal to any element. It will fade and animate in when it enters the viewport. Choose from several animation variants using the attribute value.
fade (default), slide-up, slide-down, slide-left, slide-right, zoom, flipdata-dh-stagger on a container to auto-delay each child elementdata-dh-delay="200" (ms) on individual elements to set a custom delayprefers-reduced-motion is active<!-- Default fade-in -->
<div data-dh-reveal>Fades in</div>
<!-- Slide up from below -->
<div data-dh-reveal="slide-up">Slides up</div>
<!-- Zoom in -->
<div data-dh-reveal="zoom">Zooms in</div>
<!-- Auto-staggered container -->
<div class="dh-grid-3" data-dh-stagger>
<div class="dh-card--dark" data-dh-reveal="slide-up">Card 1</div>
<div class="dh-card--dark" data-dh-reveal="slide-up">Card 2</div>
<div class="dh-card--dark" data-dh-reveal="slide-up">Card 3</div>
</div>
<!-- Manual delay on a single element -->
<div data-dh-reveal="fade" data-dh-delay="400">Delayed 400 ms</div>
dotHello ships a complete dark-mode system. It reads localStorage and prefers-color-scheme on load, adds .dh-dark to <html> when dark mode is active, and persists the user's choice. Use the dh-dark-toggle element for a UI control, or drive it programmatically.
<button class="dh-dark-toggle">
<span class="dh-dark-toggle__icon"></span>
<span class="dh-dark-toggle__label">Dark mode</span>
</button>
DH.getDarkMode(); // → true or false
DH.setDarkMode(true); // force dark mode on
DH.setDarkMode(false); // force light mode
DH.toggleDarkMode(); // flip the current state
An auto-generated audio player with a track list. Use data-dh-audio="classic" for an iPod-style design or "modern" for a minimal bar style. Provide tracks as a JSON array in data-dh-audio-tracks.
<div data-dh-audio="classic"
data-dh-audio-tracks='[
{"title":"Track One","artist":"Artist Name","src":"audio/track1.mp3"},
{"title":"Track Two","artist":"Artist Name","src":"audio/track2.mp3"}
]'>
</div>
src paths must point to real audio files. Supported formats: MP3, OGG, WAV.Add data-dh-countdown with an ISO 8601 date string. The timer counts down to that date and fires a dh:countdown:end custom event on the element when it reaches zero.
<div class="dh-countdown" data-dh-countdown="2026-12-31T00:00:00">
<div class="dh-countdown__inner">
<div class="dh-countdown__unit">
<span class="dh-countdown__value">00</span>
<span class="dh-countdown__label">Days</span>
</div>
<span class="dh-countdown__sep">:</span>
<div class="dh-countdown__unit">
<span class="dh-countdown__value">00</span>
<span class="dh-countdown__label">Hours</span>
</div>
<span class="dh-countdown__sep">:</span>
<div class="dh-countdown__unit">
<span class="dh-countdown__value">00</span>
<span class="dh-countdown__label">Minutes</span>
</div>
<span class="dh-countdown__sep">:</span>
<div class="dh-countdown__unit">
<span class="dh-countdown__value">00</span>
<span class="dh-countdown__label">Seconds</span>
</div>
</div>
</div>
<!-- Listen for completion -->
<script>
document.querySelector('.dh-countdown')
.addEventListener('dh:countdown:end', () => console.log('Done!'));
</script>
Add data-dh-tabs to a wrapper element. Tab buttons use data-tab attributes and panels use data-tab-panel. Arrow keys navigate between tabs and full ARIA roles are applied automatically.
<div data-dh-tabs>
<div class="dh-tabs__nav">
<button data-tab="tab1" class="is-active">Tab One</button>
<button data-tab="tab2">Tab Two</button>
<button data-tab="tab3">Tab Three</button>
</div>
<div data-tab-panel="tab1" class="is-active">Content for Tab One</div>
<div data-tab-panel="tab2">Content for Tab Two</div>
<div data-tab-panel="tab3">Content for Tab Three</div>
</div>
Add data-dh-accordion for single-open mode or data-dh-accordion="multi" to allow multiple panels open at once. Each item needs a trigger button and a body panel.
<!-- Single open (default) -->
<div data-dh-accordion>
<div class="dh-accordion__item">
<button class="dh-accordion__trigger">Question one?</button>
<div class="dh-accordion__body">Answer text goes here.</div>
</div>
<div class="dh-accordion__item">
<button class="dh-accordion__trigger">Question two?</button>
<div class="dh-accordion__body">Another answer here.</div>
</div>
</div>
<!-- Multi-open -->
<div data-dh-accordion="multi">
<!-- same item structure -->
</div>
Wire up a trigger button with data-dh-modal-open pointing to the modal's id. Close buttons inside use data-dh-modal-close. Clicking the backdrop or pressing Escape also closes. Full focus trap and ARIA applied automatically.
<!-- Trigger -->
<button class="dh-btn dh-btn--primary" data-dh-modal-open="my-modal">
Open modal
</button>
<!-- Modal markup -->
<div id="my-modal" class="dh-modal">
<div class="dh-modal__box">
<h2 class="dh-modal__title">Modal title</h2>
<p>Modal body content goes here.</p>
<button class="dh-btn dh-btn--ghost-dark dh-btn--sm" data-dh-modal-close>
Close
</button>
</div>
</div>
Add data-dh-dropdown to the wrapper. The trigger button and content panel use the classes below. Escape or clicking outside closes the dropdown; arrow keys navigate items.
<div data-dh-dropdown class="dh-dropdown">
<button class="dh-btn dh-btn--ghost-dark dh-dropdown__trigger">
Options ▾
</button>
<div class="dh-dropdown__content">
<a href="#">Edit</a>
<a href="#">Duplicate</a>
<a href="#">Delete</a>
</div>
</div>
Add data-dh-lightbox to any <img> to make it openable in a full-screen lightbox. Group images into a gallery by wrapping them in a data-dh-gallery container — arrow keys and swipe then navigate between them.
<img data-dh-lightbox src="photo.jpg" alt="A description">
<div data-dh-gallery class="dh-grid-3">
<img data-dh-lightbox src="photo1.jpg" alt="Image 1">
<img data-dh-lightbox src="photo2.jpg" alt="Image 2">
<img data-dh-lightbox src="photo3.jpg" alt="Image 3">
</div>
Add data-dh-progress="75" (0–100) to a dh-progress element. Add data-dh-progress-animated to animate the bar in when it scrolls into view.
<!-- Static -->
<div class="dh-progress" data-dh-progress="65">
<div class="dh-progress__bar"></div>
<span class="dh-progress__label">65%</span>
</div>
<!-- Animated on scroll -->
<div class="dh-progress" data-dh-progress="90" data-dh-progress-animated>
<div class="dh-progress__bar"></div>
<span class="dh-progress__label">90%</span>
</div>
Wrap skeleton placeholder elements in a container with data-dh-skeleton. JS will remove them after data-dh-skeleton-delay ms (default 2000). Call DH.removeSkeleton(el) programmatically when real data has loaded.
<div data-dh-skeleton data-dh-skeleton-delay="3000">
<div class="dh-skeleton dh-skeleton--avatar"></div>
<div class="dh-skeleton dh-skeleton--text"></div>
<div class="dh-skeleton dh-skeleton--text" style="width:60%"></div>
<div class="dh-skeleton dh-skeleton--card"></div>
</div>
fetch('/api/data').then(res => res.json()).then(data => {
DH.removeSkeleton(document.querySelector('[data-dh-skeleton]'));
// render real content…
});
Add data-dh-validate to any <form> for automatic validation on submit with inline error messages. Add data-dh-validate="submit-only" to suppress real-time validation. Call DH.validateForm() programmatically for manual control.
<form data-dh-validate>
<div class="dh-form__group">
<label class="dh-form__label" for="email">Email</label>
<input class="dh-input" type="email" id="email" required>
<div class="dh-form__error"></div>
</div>
<button class="dh-btn dh-btn--primary" type="submit">Submit</button>
</form>
const form = document.querySelector('form');
DH.validateForm(form, { realtime: true, showErrors: true });
Call DH.toast() from anywhere in your JS. The toast container is auto-created. Toasts auto-dismiss after duration ms (default 3500).
// Default (dark)
DH.toast('Changes saved.');
// Success
DH.toast('Profile updated!', { type: 'success', duration: 4000 });
// Error
DH.toast('Something went wrong.', { type: 'error' });
// Warning
DH.toast('Your session expires soon.', { type: 'warning', duration: 6000 });
Add data-dh-video to any element with a youtube:VIDEO_ID or vimeo:VIDEO_ID value. A placeholder thumbnail is shown; clicking embeds the iframe with autoplay.
<!-- YouTube -->
<div data-dh-video="youtube:dQw4w9WgXcQ"></div>
<!-- Vimeo -->
<div data-dh-video="vimeo:148751763"></div>
Add data-dh-parallax to any element to give it a parallax scrolling offset. Control the speed with data-dh-parallax-speed — 0 means no movement, 1 moves at full scroll speed. Default is 0.5.
<!-- Default speed (0.5) -->
<div data-dh-parallax>Subtle parallax</div>
<!-- Slow drift -->
<div data-dh-parallax data-dh-parallax-speed="0.2">Slow</div>
<!-- Fast drift -->
<div data-dh-parallax data-dh-parallax-speed="0.8">Fast</div>
Add data-dh-lottie with the path to a .json animation file. Requires lottie-web to be loaded (via CDN or locally). Configure playback with the optional attributes below.
<!-- Load lottie-web before dothello.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js"></script>
<!-- Animation element -->
<div data-dh-lottie="animations/hero.json"
data-dh-lottie-autoplay="true"
data-dh-lottie-loop="true"
data-dh-lottie-speed="1"
data-dh-lottie-renderer="svg">
</div>
DH.initSmoothScroll() intercepts all <a href="#anchor"> links on the page and scrolls smoothly to the target element instead of jumping. No markup changes needed — it activates automatically via DH.init().
<!-- Any anchor link is automatically smooth -->
<a href="#features">Jump to features</a>
<section id="features">...</section>
DH.swapPanel(from, to, onShown) transitions between two visible panel elements with a smooth crossfade. Pass the two DOM elements and an optional callback fired after the new panel is visible.
const step1 = document.getElementById('step-1');
const step2 = document.getElementById('step-2');
document.getElementById('next-btn').addEventListener('click', () => {
DH.swapPanel(step1, step2, () => {
console.log('Step 2 is now visible');
});
});
Automatically injects a Copy button into every .dh-code-block. Clicking copies the code to the clipboard and shows a 2-second "Copied!" confirmation. Called by DH.init() — no setup required.
<div class="dh-code-block" data-dh-lang="JavaScript">
<pre><code>console.log('hello');</code></pre>
</div>
The optional data-dh-lang attribute sets the language label displayed in the copy button.
Add data-dh-autotext to any element and dotHello will automatically pick a readable foreground colour based on the element's background. Works with solid colours, images (canvas sampling), and gradients (CSS blend fallback). Re-runs on every dark-mode toggle.
<!-- Solid background — YIQ contrast -->
<section class="dh-hero" data-dh-autotext>...</section>
<!-- Custom token overrides instead of #fff / #000 -->
<div style="background:#c8b560" data-dh-autotext
data-dh-light="var(--dh-color-white)"
data-dh-dark="var(--dh-color-dark)">...</div>
Tiers: (1) solid bg-color → YIQ formula · (2) bg-image → canvas pixel sampling · (3) gradient detected → dh-text--blend CSS class added automatically.
Public helper functions available on the DH namespace for use in template scripts.
// YIQ contrast — returns '#ffffff' or '#000000' (threshold 186)
DH.getContrastColor('#6C63FF'); // → '#ffffff'
DH.getContrastColor('#FFDD57', '#fff', '#1a1a1a'); // custom pair
// Convert computed color string to {r,g,b}
DH.parseRGB(getComputedStyle(el).backgroundColor);
// Walk DOM for nearest non-transparent background
DH.getEffectiveBg(document.querySelector('.dh-hero'));
// Convert r,g,b integers to hex string
DH.rgbToHex(108, 99, 255); // → '#6c63ff'
The dashboard addon is a separate CSS + JS pair that adds charts, sortable tables, stat trends, layout primitives, and a full 12-column dashboard grid. Load it after the core dotHello files.
<!-- 1. Core -->
<link rel="stylesheet" href="dothello.css">
<!-- 2. Dashboard addon -->
<link rel="stylesheet" href="dothello-dashboard.css">
<!-- 3. Core -->
<script src="dothello.js"></script>
<!-- 4. Dashboard addon -->
<script src="dothello-dashboard.js"></script>
<script>
DH.init(); // core behaviours
DH.initDashboard(); // charts + tables
</script>
SVG charts rendered from inline JSON via data-dh-chart. Supported types: line, area, bar, donut. All charts include hover tooltips and automatically pick colours from the --dh-chart-0…--dh-chart-11 CSS token cycle.
<div data-dh-chart="area"
data-dh-chart-data='{
"labels": ["Jan","Feb","Mar","Apr","May","Jun"],
"datasets": [
{ "label": "Visitors", "values": [1200,1800,1600,2400,2100,2800] }
]
}'
style="height:220px"></div>
For donut charts, supply a single dataset with an optional "colors" array to override the auto palette.
Add data-dh-table to a <table> and data-dh-sort to any <th> to make columns sortable. Wire up a search input with data-dh-table-filter="tableId".
<input class="dh-table__search" data-dh-table-filter="pages-table"
placeholder="Filter…">
<div class="dh-table-wrap">
<table class="dh-table" data-dh-table id="pages-table">
<thead>
<tr>
<th data-dh-sort>Page</th>
<th data-dh-sort>Views</th>
<th>Trend</th>
</tr>
</thead>
<tbody>
<tr><td>/home</td><td>4821</td>
<td><span class="dh-badge--trend dh-badge--trend-up">↑ 12%</span></td></tr>
</tbody>
</table>
</div>
Use .dh-dashboard-grid with .dh-col-* span helpers (1–12) for responsive 12-column layouts. Collapses to single column below 640 px.
<div class="dh-dashboard">
<!-- Sidebar (from dothello.css) -->
<aside class="dh-sidebar">...</aside>
<!-- Main content -->
<main class="dh-dashboard__main">
<header class="dh-topbar">
<span class="dh-topbar__title">Analytics</span>
</header>
<div class="dh-dashboard-grid">
<div class="dh-col-3"><!-- stat card --></div>
<div class="dh-col-3"><!-- stat card --></div>
<div class="dh-col-3"><!-- stat card --></div>
<div class="dh-col-3"><!-- stat card --></div>
<div class="dh-col-8"><!-- line chart --></div>
<div class="dh-col-4"><!-- donut chart --></div>
<div class="dh-col-12"><!-- table --></div>
</div>
</main>
</div>
DH.initDashboard(); // runs initCharts() + initDashboardTables()
// Or individually:
DH.initCharts(); // render all [data-dh-chart] elements
DH.initDashboardTables(); // wire all [data-dh-table] elements
See dashboard-demo.html for a fully working example with stat cards, area chart, bar chart, donut, and sortable table.