No dependencies · No build step

One framework.
Three pillars. Pick any.

Keystone gives you styles, icons, and behavior as three independent layers — each a single include that works from a CDN or a local file, on its own or together. Charting comes as an optional fourth include, so pages that don't chart pay nothing. The behavior layer carries what data apps keep rewriting: paged and selectable tables, async region states, a command palette, and a print sheet.

Get started View live demo

Why Keystone?

Small, honest, and dependency-free. Use exactly the parts you want.

Mix & match

Want our icons but not our styles? Our JS but your own CSS? Every pillar stands alone and degrades gracefully when the others are absent.

Zero dependencies

No frameworks, no runtime, no bundler. The behavior layer is vanilla JavaScript written from scratch. Drop the files in and go.

CDN or local

The same single tag works whether you link to a CDN or download the files. Icons embed their font, so there's no assets folder to chase.

Token-driven

Everything is built on --ks-* CSS custom properties. Retheme by overriding variables; dark mode ships in the box.

Accessible by default

Focus management, ARIA wiring, keyboard support, and reduced-motion respect are built into the components — not bolted on.

Extensible

Add your own components with a buildless plugin system: drop a module, list it in config.json, and it loads at runtime.

The three pillars

Each is one include. Use one, two, or all three.

1

Style

Design tokens and component classes — buttons, forms, cards, modals, tabs, tables and more.

<link rel="stylesheet" href="keystone.css">
2

Icons

601 geometric icons in one self-contained file — the webfont is base64-embedded, so there's nothing else to download.

<link rel="stylesheet" href="keystone-icons.css">
3

Behavior

From-scratch components: modals, drawers, dropdowns, tabs, accordions, tooltips, toasts, theme & form helpers.

<script type="module" src="keystone.js"></script>
Optional module

Charts

Bar, line, area, donut and sparklines as inline SVG — no charting library. Charts are built from a real <table>, so without JS the data still renders, and with it the table stays reachable behind “View data” as the chart's screen-reader representation. Series colors come from the colorblind-safe --ks-chart-1…8 slots and follow the theme automatically.

<link rel="stylesheet" href="keystone-charts.css">
<script type="module" src="keystone-charts.js"></script>
Revenue by quarter
QuarterManaged ITCloudSecurity
Q148,29021,4009,800
Q251,12024,90012,300
Q346,70028,15014,050
Q454,38030,60015,900

Built for data apps

Dashboards spend most of their code on the same few problems. These are in the pillars you already loaded — no extra include, no extra file.

The table pack

Sorting, client-side paging, row selection, column show/hide and CSV / clipboard export — four behaviors over one controller, so they compose instead of fighting. Rows arriving later from an API are picked up automatically. Without JS it's one plain, complete table.

<table class="ks-table" id="tickets"
       data-ks-table-sortable
       data-ks-table-select
       data-ks-table-page="5">
HostTechOpen
store5-pfsenseAdam Wilson62
nc-web-serverChase Merwin45
mow-sqlTaner Russo44
ue-server-2024Andrew Paris34
store-backup-01Adam Wilson28
adam-pcChase Merwin21
ops-store5Taner Russo17

Async regions

The four-state dance every data screen does — loading, error + retry, empty, data — as markup instead of app code, with the poll timer and the “updated 2m ago” stamp handled. Keystone owns the states; you own the fetch. A failed refresh keeps your last good numbers on screen instead of blanking the board.

Keystone.resource('#problems', {
  load: () => myApi.activeProblems(),
  poll: 60000,
});
Active problems
Couldn't reach the API
All clear
HostProblemSince

Command palette

Register commands, get ⌘K and real keyboard shortcuts. It only binds once your page registers something, so pages that don't use it keep the shortcut.

Relative time

<time data-ks-relative> reads “” and keeps itself current on one shared tick that stops when the tab is hidden. The date you author stays the no-JS fallback.

Print & clipboard

A dashboard prints as a report: light palette, hairlines, no chrome, charts with their data tables. And data-ks-copy on any button copies a snippet — every Copy button on this page is one.

Get started

Link to it, or download it. Then write plain HTML.

1 · Add Keystone

Drop these in your <head>. Keep only the pillars you need.

<link rel="stylesheet" href="https://ui.aheliotech.app/keystone.css">
<link rel="stylesheet" href="https://ui.aheliotech.app/keystone-icons.css">
<script type="module" src="https://ui.aheliotech.app/keystone.js"></script>

<!-- Optional: only on pages that chart -->
<link rel="stylesheet" href="https://ui.aheliotech.app/keystone-charts.css">
<script type="module" src="https://ui.aheliotech.app/keystone-charts.js"></script>

Served from ui.aheliotech.app. Pin a version when releases are tagged.

2 · Write HTML

Components are plain classes and data-ks-* hooks. The behavior layer wires them up automatically.

<body class="ks-body ks-scope">
  <button class="ks-btn ks-btn--primary" data-ks-modal-open="hello">
    <span class="ks-i ks-i-controls-search"></span> Open
  </button>

  <div class="ks-modal" data-ks-modal id="hello">
    <div class="ks-modal-dialog">
      <div class="ks-modal-header">
        <h3 class="ks-modal-title">Hello</h3>
        <button class="ks-modal-close" data-ks-modal-close></button>
      </div>
      <div class="ks-modal-body">It works.</div>
    </div>
  </div>
</body>

3 · Try it

These controls are live — rendered by the same Keystone files this page loads.

Hello from Keystone

This dialog is rendered by keystone.js — focus is trapped, Esc closes it, and the backdrop dismisses on click.

It works.


Download

Grab individual files, or take the whole keystone/ folder.

Optional charts module — add both files only on pages that chart.

Extend it with plugins

No build, no bundler. Drop a module, list it, and it loads at runtime.

plugins/config.json

[
  { "path": "sample/main.js", "enabled": true },
  { "path": "my-plugin/main.js" }
]

plugins/my-plugin/main.js

export default function register(ks) {
  ks.register('greeter', {
    selector: '[data-greet]',
    mount(el) {
      ks.utils.on(el, 'click', () =>
        ks.toast({ message: 'Hi!' }));
    },
  });
}

The core fetches config.json relative to keystone.js and imports each module in the order you list. A missing config is harmless.

Truly independent

No pillar requires another. Load whatever fits.

LoadedResult
Icons only.ks-i-* glyphs render.
Style onlyComponents fully styled; interactive ones sit in their default state.
Behavior onlyBehaviors work; injected marks are inline SVG; appearance is unstyled.
Any combinationProgressive enhancement, with no errors when a pillar is absent.

Brand

A two-tone bracketed [KS] — technical and corporate. Download the assets below.