Components
The building blocks every page is made of. Each example below is live — it uses the template's real CSS and JavaScript — with its markup underneath, ready to copy.
Before you start
- The components are styled by
css/style.css. Most need no JavaScript; the ones that do say so. - Colours come from the shared status palette: add
success,danger,warning,info,violet,lightordarkto switch a component's colour. See Status colours. - Each section links to its reference page in the template, which shows every variant.
Cards
Almost everything on a page sits in a card: a header with a title (and optional actions on the right), then a body. Reference page
<div class="card">
<div class="card-header">
<span>Recent orders</span>
<button type="button" class="btn-soft btn-sm"><i class="fas fa-download"></i> Export</button>
</div>
<div class="card-body">Card content goes here.</div>
</div>
Place cards in Bootstrap's grid — <div class="row g-4"> with columns such as col-xl-6 — so they line up with the rest of the template. Cards in a row are equal height; .card-auto turns that off.
All backups are up to date
Last backup 03:00The next backup runs tonight.
<div class="card card-accent success">
<div class="card-header">
<span>Backups</span>
<div class="card-tools">
<button type="button" class="icon-btn" data-bs-toggle="collapse" data-bs-target="#backup-body"
aria-expanded="true" aria-label="Collapse"><i class="fas fa-chevron-down"></i></button>
<button type="button" class="icon-btn danger" data-dismiss="card" aria-label="Close"><i class="fas fa-times"></i></button>
</div>
</div>
<div class="collapse show" id="backup-body">
<div class="card-body">
<h5 class="card-title">All backups are up to date</h5>
<span class="card-subtitle">Last backup 03:00</span>
<p class="card-text">The next backup runs tonight.</p>
</div>
</div>
<div class="card-footer">
<button type="button" class="btn-soft btn-sm">Settings</button>
<button type="button" class="btn-brand btn-sm">Back up now</button>
</div>
</div>
- Text:
.card-title,.card-subtitle,.card-text,.card-link. Footer:.card-footer(buttons on the right) or.card-footer.is-text(text left, link right); add.d-flex.flex-columnto the card to keep the footer at the bottom. - Images:
.card-media(16:9 cover) above the body, or.card-rowwith the image on the left..card-hover+ a.stretched-linkmakes the whole card clickable. - Lists:
<ul class="card-list">right after the header — rows with dividers, flush with the edges. Tables:.card-body.p-0with.table-custom.table-padded. - Colour:
.card-accent(thin line on top) or.card-soft(soft fill), plus.success,.warning,.danger,.infoor.violet. - Tools:
.card-toolsin the header; collapse with Bootstrap'sdata-bs-toggle="collapse"on a.collapse.showbody;data-dismiss="card"removes the card (js/app.js).
Buttons
Four button styles, each in every palette colour. Use them on <button> or <a>. Reference page
| Class | Looks like | Use for |
|---|---|---|
.btn-brand | Solid fill | The main action of a page or form |
.btn-dim | Soft fill, solid on hover | Secondary actions that still need colour |
.btn-outline | White with a coloured border | Quiet alternatives, e.g. "Remove" |
.btn-soft | Neutral grey | Cancel, Back, Export and other neutral actions |
<button type="button" class="btn-brand">Save</button>
<button type="button" class="btn-brand success">Publish</button>
<button type="button" class="btn-dim">Preview</button>
<button type="button" class="btn-outline danger">Remove</button>
<button type="button" class="btn-soft">Cancel</button>
<a href="#" class="btn-ghost">View all <i class="fas fa-arrow-right"></i></a> <!-- text link button -->
Sizes, shapes and icons
<button type="button" class="btn-brand btn-sm">Small</button>
<button type="button" class="btn-brand btn-lg">Large</button>
<button type="button" class="btn-dim btn-pill success"><i class="fas fa-upload"></i> Upload</button>
<!-- icon only: always give it an aria-label -->
<button type="button" class="btn-brand btn-square" aria-label="Add"><i class="fas fa-plus"></i></button>
<!-- small row actions in tables and lists -->
<button type="button" class="icon-btn" aria-label="Edit"><i class="fas fa-pen"></i></button>
<button type="button" class="icon-btn danger" aria-label="Delete"><i class="far fa-trash-alt"></i></button>
Also available: .btn-block (full width), and disabled on a button or aria-disabled="true" on a link. Group buttons in a .btn-row for an even 8px gap.
Loading state
Put a hidden Bootstrap spinner inside the button and show it while the action runs:
<button type="submit" class="btn-brand" id="saveBtn">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" hidden></span>
<span>Save</span>
</button>
// while saving
saveBtn.disabled = true;
saveBtn.querySelector('.spinner-border').hidden = false;
Groups, split buttons and toggles
<!-- joined buttons; aria-pressed="true" marks the chosen one -->
<div class="btn-group" role="group" aria-label="Period">
<button type="button" class="btn-soft">Day</button>
<button type="button" class="btn-soft" aria-pressed="true">Week</button>
<button type="button" class="btn-soft">Month</button>
</div>
<!-- split button: main action + a Bootstrap dropdown -->
<div class="btn-group">
<button type="button" class="btn-brand">Publish</button>
<button type="button" class="btn-brand btn-split" data-bs-toggle="dropdown" aria-label="More options">
<i class="fas fa-chevron-down"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end card-action-menu">…</ul>
</div>
<button type="button" class="btn-soft">Comments <span class="badge-count soft">12</span></button>
.btn-group-vertical stacks the buttons; several groups in a .btn-row make a toolbar. A .btn-soft with aria-pressed="true" (or .active) stays highlighted — toggle the attribute in your script for on / off buttons. For a dropdown on its own, put data-bs-toggle="dropdown" on any button inside a .dropdown.
Alerts
Messages inside the page. Four styles — .alert-soft, .alert-solid, .alert-outline and .alert-accent (a coloured bar on the left) — each in every palette colour. Reference page
Drag rows by the handle to change their order.
<div class="alert-soft success" role="alert">
<i class="fas fa-check-circle"></i>
<div class="alert-body"><strong>Saved.</strong> The page “About Us” was published.</div>
</div>
<!-- with a title -->
<div class="alert-accent" role="alert">
<i class="fas fa-bell"></i>
<div class="alert-body">
<strong class="alert-title">Tip</strong>
<p>Drag rows by the handle to change their order.</p>
</div>
</div>
<!-- closable (needs js/alerts.js) -->
<div class="alert-outline danger" role="alert">
<i class="fas fa-times-circle"></i>
<div class="alert-body">The upload failed. Try again.</div>
<button type="button" class="alert-close" data-dismiss="alert" aria-label="Close"><i class="fas fa-times"></i></button>
</div>
A closed alert is removed from the page. Add data-hide to the alert to only hide it, so you can show it again later. Extra sizes: .alert-sm for a compact line, .alert-banner for a full-width strip at the top of a card.
Toasts
Short pop-up messages in a corner of the screen. They need js/alerts.js. Toastr page (builder and all options)
<button type="button" class="btn-soft" data-toast="success"
data-title="Page published" data-text="“About Us” is now live.">Show a toast</button>
From JavaScript — for example to show a message from your server after saving:
Odin.toast({
type: 'success', // primary | success | warning | danger | info | light | dark
title: 'Saved',
text: 'Your changes are live.',
timeout: 5000, // ms; 0 = stays until closed
position: 'top-right' // top-right | top-center | top-left | bottom-right | bottom-center | bottom-left
});
// shortcuts: text, title, options — also .info .warning .danger (.error) .primary .light .dark
Odin.toast.success('Your changes are live.', 'Saved');
Odin.toast.warning('3 pages have no translation.', '', { timeout: 0, closeOnClick: true });
// a toast you close yourself
const t = Odin.toast.primary('Uploading…', '', { timeout: 0, closeButton: false });
t.close();
Odin.toast.clear(); // closes all (or .clear('bottom-right'))
// change a default once for the whole admin
Odin.Toast.defaults.position = 'bottom-right';
More options: closeButton (default on), progressBar (on), newestOnTop (on), preventDuplicates, closeOnClick, onClick: toast => … and icon. Titles and texts are plain text, never HTML.
Badges
Small labels for statuses, counts and tags: .badge-soft, .badge-solid and .badge-outline, in every palette colour. Reference page
<span class="badge-soft">New</span>
<span class="badge-soft success badge-pill badge-dot">Published</span> <!-- pill shape with a dot -->
<span class="badge-solid danger">Overdue</span>
<span class="badge-outline info">Scheduled</span>
<span class="badge-count danger">3</span> <!-- round counter -->
<!-- removable tag -->
<span class="badge-soft badge-pill">Design
<button type="button" class="badge-x" data-dismiss="badge" aria-label="Remove Design"><i class="fas fa-times"></i></button>
</span>
Sizes: .badge-sm and .badge-lg. The reference page also has counters pinned to icons (.badge-anchor + .badge-pip), online/away dots on avatars (.presence) and the table status pill (.badge-status).
Breadcrumbs
Bootstrap's breadcrumb, restyled: muted links, a light separator and the current page in dark text. Reference page
<nav aria-label="breadcrumb">
<ol class="breadcrumb"> <!-- + breadcrumb-arrow for chevrons -->
<li class="breadcrumb-item"><a href="index.html">Dashboard</a></li>
<li class="breadcrumb-item"><a href="pages-list.html">Pages</a></li>
<li class="breadcrumb-item active" aria-current="page">Edit Page</li>
</ol>
</nav>
Modifiers on the <ol>: .breadcrumb-arrow or .breadcrumb-dot (separator), .breadcrumb-sm (for cards and side panels), .breadcrumb-truncate (one line, a long title ends with "…") and .breadcrumb-responsive (on phones only a "‹ Parent" back link).
Accordions
Collapsible panels for FAQs and settings. They use Bootstrap's collapse, so no extra JavaScript. Reference page
Yes — one Pro license covers one end product.
No. Every page is plain HTML, CSS and JavaScript.
<div class="acc" id="faq"> <!-- or: acc acc-flush (no frame), + acc-icon-left -->
<div class="acc-item">
<button class="acc-head" type="button" data-bs-toggle="collapse" data-bs-target="#faq-1"
aria-expanded="true" aria-controls="faq-1">
<span class="acc-title">Can I use the template in client projects?</span>
<span class="acc-icon" aria-hidden="true"></span>
</button>
<div id="faq-1" class="acc-body collapse show" data-bs-parent="#faq">
<div class="acc-inner"><p>Yes — one Pro license covers one end product.</p></div>
</div>
</div>
…
</div>
data-bs-parent keeps only one panel open; remove it to let panels open independently. An open panel has collapse show and aria-expanded="true" on its button. Ids must be unique on the page.
Tabs
Horizontal tabs (.tabs) and a vertical list (.tabs-vertical), switched by Bootstrap's tab plugin. Reference page
<ul class="tabs" role="tablist"> <!-- or class="tabs-vertical" aria-orientation="vertical" -->
<li role="presentation">
<button class="tab-link active" type="button" role="tab" data-bs-toggle="tab"
data-bs-target="#tab-general" aria-controls="tab-general" aria-selected="true">
<i class="fas fa-cog"></i><span>General</span> <!-- icon optional -->
</button>
</li>
…
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-general" role="tabpanel" tabindex="0">…</div>
…
</div>
Keep role="tab" on the buttons — Bootstrap finds the tab list through it. Two more options: .tabs-sm for compact tabs inside a form field (one tab per language, say), and .has-error on a tab whose panel holds a validation error. Inside a card, put horizontal tabs in .card-body.pt-0 so they sit right under the header.
Modals
Bootstrap's modal, restyled: no border, a soft shadow, a light footer. Reference page
<button type="button" class="btn-brand" data-bs-toggle="modal" data-bs-target="#renameModal">Open</button>
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModal-title" aria-hidden="true">
<div class="modal-dialog"> <!-- + modal-sm | modal-lg | modal-xl | modal-dialog-centered -->
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameModal-title">Rename Category</h5>
<button type="button" class="modal-close" data-bs-dismiss="modal" aria-label="Close"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<label class="form-label" for="catName">Category name</label>
<input type="text" class="form-control form-soft" id="catName" value="Business">
</div>
<div class="modal-footer">
<button type="button" class="btn-soft" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn-brand">Save</button>
</div>
</div>
</div>
</div>
Confirm dialogs
For "Are you sure?" questions the template uses SweetAlert2, themed to match, through js/modals.js. Load sweetalert2.min.css before css/style.css, and sweetalert2.min.js — not the .all build — before js/modals.js. Then a delete button only needs data attributes:
<button type="button" class="icon-btn danger" aria-label="Delete About Us"
data-confirm="“About Us” will be moved to the trash."
data-confirm-title="Delete this page?" data-confirm-button="Delete">
<i class="far fa-trash-alt"></i>
</button>
On a link, the browser follows the href after "Delete"; on a submit button, the form is sent. Otherwise listen for the odin:confirmed event. From your own code: if (await Odin.Dialog.confirm({ title, text, confirmText: 'Delete' })) { … }.
Tooltips
Any element with data-bs-toggle="tooltip" and a title gets a tooltip — js/app.js starts them on every page. Reference page
<button type="button" class="icon-btn" data-bs-toggle="tooltip" title="Edit" aria-label="Edit">
<i class="fas fa-pen"></i>
</button>
<!-- placement and the light style -->
<button … data-bs-toggle="tooltip" data-bs-placement="bottom" title="Duplicate">…</button>
<button … data-bs-toggle="tooltip" data-bs-custom-class="tooltip-light" title="A light tooltip">…</button>
<!-- help icon next to a label -->
<label class="form-label" for="slug">Slug
<button type="button" class="tip-icon" data-bs-toggle="tooltip"
title="Lowercase letters, numbers and hyphens." aria-label="About the slug">
<i class="far fa-question-circle"></i>
</button>
</label>
Keep aria-label on icon-only buttons — the tooltip is a visual hint, not the button's name. A disabled button can't show a tooltip; wrap it in <span tabindex="0" data-bs-toggle="tooltip">. For content added after the page loads, call Odin.init(container) to start its tooltips.
Dropdowns
Bootstrap dropdowns, restyled. The most common one is the "⋯" menu in a card header or a table row.
<div class="dropdown">
<button class="row-action" type="button" data-bs-toggle="dropdown" aria-expanded="false" aria-label="More actions">
<i class="fas fa-ellipsis-h"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end card-action-menu">
<li><a class="dropdown-item" href="#"><i class="fas fa-sync-alt"></i> Refresh</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-download"></i> Download CSV</a></li>
<li><hr class="dropdown-divider mx-2"></li>
<li><a class="dropdown-item danger" href="#"><i class="fas fa-trash-alt"></i> Remove</a></li>
</ul>
</div>
dropdown-menu-end lines the menu up with the right edge of the button — use it for menus at the right of a card or row. .danger on an item colours it red.
Tables
Tables use Bootstrap's .table with the template's .table-custom look. Add js/tables.js and a few data attributes, and the same table gets search, filters, sorting and paging in the browser. Reference page
| ID | Title | Status | Created |
|---|---|---|---|
| #1048 | Our 2026 roadmapCompany News | Published | 08 Oct 2026 |
| #1047 | How we cut page load timesEngineering | Published | 06 Oct 2026 |
| #1046 | Writing for the webGuides | Draft | 03 Oct 2026 |
| #1045 | Autumn product updateReleases | Scheduled | 29 Sep 2026 |
| #1044 | Five tips for better formsDesign | Published | 22 Sep 2026 |
<div class="card card-auto" data-table="posts" data-per-page="10">
<div class="card-header"><span>Blog Posts</span></div>
<div class="table-toolbar">
<div class="tt-filters">
<div class="input-icon tt-search">
<i class="fas fa-search"></i>
<input type="search" class="form-control form-soft" placeholder="Search posts…" aria-label="Search posts" data-search>
</div>
<select class="form-select form-soft tt-select" aria-label="Filter by status" data-filter="status">
<option value="">All statuses</option>
<option value="published">Published</option>
…
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-custom table-padded align-middle">
<thead>
<tr>
<th data-sort="id" data-sort-type="number">ID <i class="sort-ico"></i></th>
<th data-sort="title">Title <i class="sort-ico"></i></th>
<th data-sort="status">Status <i class="sort-ico"></i></th>
<th data-sort="created" data-sort-type="date">Created <i class="sort-ico"></i></th>
</tr>
</thead>
<tbody>
<!-- data-* on the row hold the raw values used to search, filter and sort -->
<tr data-row data-id="1048" data-title="Our 2026 roadmap" data-status="published" data-created="2026-10-08">
<td><span class="tbl-id">#1048</span></td>
<td><a href="#" class="tbl-title">Our 2026 roadmap</a><span class="tbl-sub">Company News</span></td>
<td><span class="badge-status success">Published</span></td>
<td class="tbl-date">08 Oct 2026</td>
</tr>
…
</tbody>
</table>
</div>
<div class="table-footer">
<span class="table-info" data-info></span> <!-- "Showing 1–10 of 48" -->
<nav class="pager" aria-label="Posts pages" data-pager></nav>
</div>
</div>
Data table options
Every part is optional — add only the hooks you need. All of them go inside the [data-table] element:
| Hook | What it does |
|---|---|
data-per-page="10" | On the root: rows per page (default 10) |
tr[data-row] | A data row. Its data-* attributes hold the values to filter and sort by |
input[data-search] | Free-text search across the row |
select[data-filter="key"] | Shows rows whose data-key matches exactly |
input[data-filter-from="key"], [data-filter-to] | A date range on data-key |
th[data-sort="key"] | A sortable column; add data-sort-type="number" or "date" |
select[data-per-page] | A rows-per-page picker |
[data-pager], [data-info] | Page buttons and "Showing x–y of n" |
[data-check-all], .row-check | Select rows; with [data-bulk-bar], [data-bulk-count], [data-bulk-delete] for bulk actions |
tr.row-details + [data-expand] | An expandable details row, right after its row |
[data-delete] | Deletes its row after a browser confirm |
input[data-col="n"] | Shows or hides column n (from 0) |
[data-export="csv"] | Downloads the filtered rows as CSV |
[data-reset] | Clears search, filters and sorting |
Browser-side paging suits a few hundred rows. For bigger tables, let your server search, filter and page (through the query string), keep the same markup, and leave out js/tables.js — or only the hooks it no longer needs. Render the page links as shown in Pagination.
Pagination
Data tables build their own page buttons. When your server renders the pages, write the links yourself with the same classes:
<nav class="pager" aria-label="Pages">
<span class="pager-btn disabled" aria-disabled="true"><i class="fas fa-chevron-left"></i></span>
<span class="pager-btn active" aria-current="page">1</span>
<a class="pager-btn" href="?page=2">2</a>
<a class="pager-btn" href="?page=3">3</a>
<a class="pager-btn" href="?page=2" rel="next" aria-label="Next page"><i class="fas fa-chevron-right"></i></a>
</nav>
Progress
Bars and circles in the palette colours. No JavaScript — set the width (or --value) and the ARIA value from your page or script. Reference page
<div class="progress-head"><span>Translated</span><strong>68%</strong></div>
<div class="progress-line" role="progressbar" aria-label="Translated"
aria-valuenow="68" aria-valuemin="0" aria-valuemax="100"><span style="width:68%"></span></div>
<!-- several parts of a total -->
<div class="progress-line progress-lg progress-stacked" role="img" aria-label="…">
<span class="success" style="width:77%"></span><span class="muted" style="width:17%"></span>
</div>
<!-- circle: the value in --value (0–100) -->
<div class="progress-ring success" style="--value:86" role="progressbar" aria-valuenow="86" …><span>86</span></div>
- Colour: a palette class on the
<span>(.success,.danger,.warning,.info,.violet,.muted);.softplus a colour class on the bar tints the track. - Height:
.progress-xs3px,.progress-sm5px, default 8px,.progress-lg12px,.progress-xl18px (room for a value inside the span). .progress-stripedadds stripes,.progress-animatedmoves them while work is running. Circles:.progress-ring-sm/-lg.
xhr.upload.onprogress = e => {
const value = Math.round(e.loaded / e.total * 100);
bar.setAttribute('aria-valuenow', value);
bar.firstElementChild.style.width = value + '%';
};
Form elements
Inputs use Bootstrap's form classes plus .form-soft for the template's look. The extra widgets — searchable selects, date pickers, uploads — are started by js/forms.js from data attributes. Reference page
Inputs, selects and checks
<form class="needs-validation" novalidate>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="name">Full name <span class="req">*</span></label>
<input type="text" class="form-control form-soft" id="name" required>
<div class="invalid-feedback">Please enter your name.</div>
</div>
<!-- input with an icon -->
<div class="col-md-6">
<label class="form-label" for="email">E-mail <span class="req">*</span></label>
<div class="input-icon">
<i class="far fa-envelope"></i>
<input type="email" class="form-control form-soft" id="email" required>
<div class="invalid-feedback">Please enter a valid e-mail address.</div>
</div>
</div>
<!-- character counter -->
<div class="col-md-6">
<div class="form-label-row">
<label class="form-label" for="bio">Short bio</label>
<span class="char-count" id="bioCount"></span>
</div>
<input type="text" class="form-control form-soft" id="bio" maxlength="60" data-count="#bioCount">
</div>
<!-- radios and a switch -->
<div class="check-inline">
<label class="check-row"><input type="radio" class="form-check-input check-soft" name="contact" checked> E-mail</label>
<label class="check-row"><input type="radio" class="form-check-input check-soft" name="contact"> Phone</label>
</div>
<label class="check-row form-switch">
<input type="checkbox" class="form-check-input check-soft" role="switch" checked> Send me monthly updates
</label>
</div>
</form>
Other helpers from js/forms.js: data-toggle-password="#id" on a button shows or hides a password field; data-slug-source="#slug" on a title input fills the slug field while the user hasn't edited it; data-indeterminate on a checkbox shows the "partly checked" state.
Validation
A form.needs-validation with novalidate uses the browser's rules (required, type="email", minlength, …) with Bootstrap's messages. On submit, invalid fields turn red and show their .invalid-feedback, the first one is focused, and tabs that hold an invalid field get .has-error. Press "Check the form" above to try it.
In the template the validator never sends the form — it's a demo. In your project, send it when it's valid:
form.addEventListener('odin:form-valid', () => form.submit());
// form.submit() skips the submit event, so the validator doesn't run twice.
// Also fired: 'odin:form-invalid'
Searchable selects (Tom Select)
Add data-ts to a <select>. Load Tom Select's CSS before css/style.css and its script before js/forms.js.
<select id="category" data-ts="default" data-placeholder="Choose a category" data-clear="true">
<option value=""></option> <!-- needed for a required single select -->
<option>Business</option>
<option>Design</option>
</select>
<select id="tags" multiple data-ts="default" data-create="true" data-placeholder="Type and press Enter">
<option selected>news</option>
<option>guides</option>
</select>
| Attribute | What it does |
|---|---|
data-ts | default, avatar (initials before each option), icon (an icon per option) or tree (indented parent/child options) |
data-placeholder | Placeholder text |
data-clear="true" | A button to clear the choice |
data-create="true" | Lets the user add new options — for tags |
data-search="false" | Hides the search box for short lists |
data-max="3" | Most items a multiple select accepts |
Date pickers (flatpickr)
Add data-fp to a text input. Load flatpickr's CSS and script the same way as Tom Select.
<div class="input-icon">
<i class="far fa-calendar"></i>
<input type="text" class="form-control form-soft" placeholder="Select a date" data-fp="date">
</div>
<!-- data-fp: date | datetime | time | range | multiple -->
<input type="text" class="form-control form-soft" data-fp="range"
data-min-date="today" data-default-date="2026-10-20, 2026-11-05">
For other languages, load flatpickr's language file (for example l10n/de.js) after flatpickr. On a page with date pickers, choosing a language in the top bar's language menu switches them; from your own code, call Odin.components.get('datePicker').setLanguage('de').
File uploads (Dropzone)
Add data-dropzone to a drop area and point data-previews at the element that lists the files.
<div class="dz-area" data-dropzone data-previews="#galleryPreviews"
data-max-files="4" data-max-size="5" data-accept="image/*" data-url="/admin/uploads">
<div class="dz-message">
<div class="dz-ico"><i class="fas fa-cloud-upload-alt"></i></div>
<span class="dz-title">Drop images here or <span>browse</span></span>
<span class="dz-hint">JPG, PNG or WebP · up to 5 MB each · max 4 files</span>
</div>
</div>
<div class="dz-previews" id="galleryPreviews"></div>
data-max-size is in MB. Without data-url the files are only previewed in the browser, as in the template; set it to your upload route to send them. Add .dz-compact for a smaller area, such as a single featured image.
Drag & drop
The template uses two drag & drop libraries: SortableJS inside its own features (pages tree, menu editor, task board) and Dragula for simple lists you add yourself, through js/dragula.js. Reference page
<div class="drag-list" data-plugin="dragula" id="homeSections">
<div class="drag-item" data-id="hero">
<div class="drag-item-body"><span class="drag-item-title">Hero Banner</span></div>
</div>
<div class="drag-item" data-id="services">
<div class="drag-item-body"><span class="drag-item-title">Services</span></div>
</div>
</div>
- Several lists that share items: put
data-plugin="dragula"on a wrapper withdata-containers='["list-a","list-b"]'(the lists' ids). data-handle-class="drag-handle"— drag only by that element.data-copy="true"— copy instead of move.[data-drag-count="#list-id"]shows how many items a list holds.
After a drop, an odin:drop event bubbles from the wrapper with { item, target, source, order } — order is the list of data-id values in the target list. Save it to your server:
document.getElementById('homeSections').addEventListener('odin:drop', e => {
fetch('/admin/home/sections/order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order: e.detail.order })
});
});
Tree view
Folders and nested items with connector lines — pages, menus, media folders, permissions. Plain JavaScript in js/tree-view.js; js/modals.js (optional) asks before deleting. Reference page
- Home
- Services
- Web Design
- SEO & Content
- Contact
- Services
<div data-tree-view data-label="Website pages">
<ul>
<li data-type="home" data-opened="true">Home
<ul>
<li data-type="folder" data-id="services" data-opened="true">Services
<ul>
<li data-type="page" data-id="web-design">Web Design</li>
<li data-type="page" data-id="seo">SEO & Content</li>
</ul>
</li>
<li data-type="page" data-id="contact">Contact</li>
</ul>
</li>
</ul>
</div>
- Item attributes:
data-id,data-type(folder, home, page, file, image, pdf, doc, link — sets the icon),data-icon(your own Font Awesome class),data-opened,data-selected,data-checked,data-disabled. - Or JSON:
data-source="#id"pointing to a<script type="application/json">with[{ id, text, type, opened, children: [...] }]— in Laravel@json($tree). - Options:
data-checkbox="true"(three-state),data-search="#input",data-dnd="true"(drag & drop, mouse),data-contextmenu="true"(New item, Rename, Delete),data-multiple="true",data-lines="false". - Keyboard: arrows move and open / close, Enter selects (or checks), F2 renames, Delete deletes, Shift + F10 opens the menu.
Save changes from the events — each has e.detail.node ({ id, text, type, parent }):
const tree = document.getElementById('pagesTree');
tree.addEventListener('odin:tree-move', e => {
const { node, parent, index } = e.detail; // index: position among the new siblings
api(`/admin/pages/${node.id}/move`, 'PATCH', { parent, index });
});
tree.addEventListener('odin:tree-rename', e => api(`/admin/pages/${e.detail.node.id}`, 'PATCH', { title: e.detail.node.text }));
// API
const view = Odin.get(tree, 'treeView');
view.getChecked(); view.getSelected(); view.openAll(); view.toJSON();
Charts
Two chart libraries are set up with the same data attributes: Chart.js (canvas, small) through js/chartjs.js and ApexCharts (SVG, more interactive) through js/apexcharts.js. Colours, font and tooltips come from the template's CSS variables. Chart.js page ApexCharts page
<!-- Chart.js: a canvas inside a .chart-box (the box sets the height) -->
<div class="chart-box">
<canvas data-plugin="chartjs" data-type="line" data-fill="true"
data-labels='["Mon","Tue","Wed"]'
data-series='[{"label":"Visitors","data":[1240,1580,1420],"color":"primary"}]'></canvas>
</div>
<!-- ApexCharts: a div, height in data-height -->
<div data-plugin="apexcharts" data-type="area" data-height="280"
data-labels='["Mon","Tue","Wed"]'
data-series='[{"label":"Visitors","data":[1240,1580,1420],"color":"primary"}]'></div>
data-type— Chart.js:line,bar,pie,doughnut,polarArea. ApexCharts:area,line,bar,donut,pie,radialBar.- Series
color:primary,success,warning,danger,info,violet,mutedor any CSS colour."dashed": truedraws a dashed line (e.g. last period). Pie, doughnut, donut and radial bar take one series with"colors": [...]. - Both:
data-curve="false"straight lines,data-stacked="true",data-horizontal="true",data-legend="false",data-prefix="$"/data-suffix="%"for values. - ApexCharts only:
"type": "bar" | "line" | "area"on a series mixes types in one chart (e.g. revenue bars with a profit line);data-compact="true"shortens axis numbers (2.4M, 180K) while tooltips stay exact. - Chart.js only:
data-fill="true"soft fill under lines;"type": "line"on a series mixes a line into a bar chart. Use.chart-box-smfor a 220px box. - ApexCharts only:
data-sparkline="true"small trend without axes (stat cards);data-total="Pages"shows a total in the middle of a donut or radial bar.
Load the library and the script after app.js. To show data from your server, render it into the attributes, or update a chart later:
fetch('/admin/stats/visitors').then(r => r.json()).then(stats => {
// 'chartjs' or 'apexcharts' — the same labels / series format as the attributes
Odin.get(document.getElementById('visitorsChart'), 'chartjs')
.update(stats.labels, [{ label: 'Visitors', data: stats.values, color: 'primary' }]);
});