v

Basis design system

The shared baseline for every app we build: design tokens, components, forms and small JavaScript behaviours, with light and dark themes. Load two files from this site and your app looks and works like the rest of the family.

2files to include
30+components
0dependencies
Light & darkout of the box

Installation

Add the stylesheet and script to your page. Both are served from this site with CORS enabled, so any domain can use them. Everything under /v1/ stays backwards compatible; breaking changes ship as /v2/.

Prefer a classic script (no modules)? Use ds.iife.js instead. Both expose the same API on window.DS.

In a Vite app

Keep the URL in an env variable so local development can point at a local copy of the design system:

Starter page

Theming

Colors are defined with light-dark(), so the whole system follows the OS theme by default. Set data-theme="light" or data-theme="dark" on <html> to force one. Any button with data-theme-value switches and remembers the theme.

To rebrand an app, override tokens after loading ds.css:

Colors

Use tokens, never raw hex values, so components adapt to both themes. Values below update when you switch theme.

Typography

System UI font first (SF Pro on Apple devices), Inter everywhere else. Base size is 14px with a 1.45 line height.

Heading 1 · 32px

Heading 2 · 24px

Heading 3 · 20px

Heading 4 · 17px

Heading 5 · 15px
Heading 6 · 13px

Body text at 14px. Links use the accent color, bold, inline code and ⌘ K.

Hint text: 12px muted, for help under fields.

TokenSizeUse
--text-xs11pxRange labels, menu labels
--text-sm12pxHints, badges, meta
--text-md13pxButtons, labels, controls
--text-base14pxBody
--text-lg17pxCard and dialog titles
--text-xl22pxPage titles
--text-2xl / --text-3xl28px / 40pxMarketing headings

Spacing

A 4px grid. Use var(--space-*) for padding, margins and gaps.

Radius & elevation

Buttons

Pill-shaped buttons. The default is a tinted secondary button; add a modifier for other styles.

Sizes, icons and states

Link as button

Icon buttons

Square buttons for a single icon. Always give them an aria-label. Use aria-pressed for toggles.

Chips

Small pill buttons for presets and filters. Mark the active one with aria-pressed="true".

Badges

Neutral Accent −42% Beta +8% New Online

Alerts

Inline messages. Add a button with data-ds-dismiss to let people close it.

Heads up

Your images are processed on this device and never uploaded.

12 images converted, 4.2 MB saved.

Storage is almost full. Older history items will be removed.

Cards

Elevated containers. Use .surface alone for a plain panel with your own layout.

photo.webp

1920×1080 · 214 KB

Storage

Saved on this device

64% 320 MB of 500 MB used

Flat card

No shadow, for nesting inside a surface.

Tabs

Add data-ds-tabs to a role="tablist". Each tab points at its panel with aria-controls. Arrow keys move between tabs.

General settings go here.

Dialog

Built on the native <dialog> element: focus trapping and Esc come for free. Open with data-ds-open="#id", close with data-ds-close. data-ds-dismissible also closes it on a backdrop click.

Clear history?

All 24 saved images will be removed from this device. This can't be undone.

The returnValue of the dialog is the value of the data-ds-close button that closed it, so you can listen for close:

Toast

Short, non-blocking notifications, created from JavaScript.

Tooltip

CSS only: add data-tooltip. Keep the text short, and don't put essential information only in a tooltip.

Accordion

Native <details>, no JavaScript. Give items the same name to allow only one open at a time.

Are my images uploaded?

No. Everything runs in your browser.

Which formats are supported?

JPEG, PNG, WebP, AVIF, HEIC, TIFF, GIF, BMP, SVG and ICO.

Is there a file size limit?

Only your device's memory.

Progress & loading

40% Done

Table

Wrap in .table-wrap for rounded corners and horizontal scrolling on small screens. Use .num for right-aligned numbers.

FileFormatBeforeAfterSaved
beach.heicWebP3.4 MB412 KB−88%
logo.pngAVIF96 KB18 KB−81%
icon.svgPNG4 KB11 KB+175%

List group

Avatar

FR AL DS A B C

Empty state

No history yet

Converted images will appear here.

Text inputs

Wrap a label and control in .field. Add .input to text, number, email, password, search, select and textarea elements.

Select & textarea

Validation

Set aria-invalid="true" on the control (or .is-invalid on the field) and add a .field-error message linked with aria-describedby.

Checkbox & radio

Switch

An iOS-style toggle: a checkbox with .switch, usually inside a .switch-row label.

Segmented control

A radio group styled as a single control. Use .segmented-grid (with --cols) to wrap many options.

Range

Native range input. ds.js fills the track as you drag; if you change value from code, call DS.syncRange(input).

Smaller fileBetter quality

Color

Drop zone

Styling for a file drop area. Toggle .is-dragover from your drag events; wire the click to a hidden file input.

Click or drag files here

PNG, JPEG or WebP, up to 50 MB

Scrollbars

Every scrollable area gets a thin, rounded scrollbar that follows the theme: no class needed. The thumb keeps a small gap from the edges, grows on hover and turns accent while dragged. Tune it with --scrollbar-size, --scrollbar-inset, --scrollbar-thumb, --scrollbar-thumb-hover and --scrollbar-thumb-active. Avoid setting scrollbar-width or scrollbar-color yourself: in Chrome that switches the custom style off.

.scroll-y

Scrolls vertically and keeps the scroll inside the box (no scroll chaining to the page).

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet.

Cras mattis consectetur purus sit amet fermentum. Donec ullamcorper nulla non metus auctor fringilla.

.scrollbar-track

A slim scrollbar with a visible track and small arrow buttons at both ends. Use it on the main scroll area of a dialog or panel.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet.

Cras mattis consectetur purus sit amet fermentum. Donec ullamcorper nulla non metus auctor fringilla.

.scrollbar-hover

The thumb only appears while the pointer is over the box. Good for sidebars and dense panels.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet.

Cras mattis consectetur purus sit amet fermentum. Donec ullamcorper nulla non metus auctor fringilla.

Utilities

A small set of helpers. Layout helpers read --gap (and --min for the grid) so you can tune them inline.

ClassDoes
.containerCentered page width (--container) with gutters
.stackVertical flex with gap
.clusterWrapping horizontal flex with gap
.spreadHorizontal flex, items pushed to both ends
.grid-autoResponsive grid of columns at least --min wide
.growTake remaining space in a flex row
.surfaceCard background, large radius and shadow
.text-muted, .text-accent, .text-success, .text-warning, .text-dangerText colors
.text-xs … .text-xl, .fw-medium, .fw-semiboldText size and weight
.tabular, .mono, .truncateTabular numbers, monospace, single-line ellipsis
.m-0, .mt-2, .mt-4, .mb-2, .mb-4, .p-4, .p-5Common spacing
.scroll-y, .scroll-xScroll container that does not chain scrolling to the page
.scrollbar-track, .scrollbar-hover, .scrollbar-noneScrollbar with a visible track, only on hover, or hidden while keeping scroll
.checkerboardTransparency backdrop for image previews
.visually-hiddenHide visually, keep for screen readers

JavaScript API

ds.js wires up behaviour through data attributes as soon as it loads; content added later works too. For anything else, use window.DS or import from the module:

FunctionDescription
DS.setTheme(theme, persist = true)'light', 'dark' or 'system'. Saved in localStorage['theme'].
DS.getTheme()The current theme setting.
DS.toast(message, options)Show a toast. Options: variant, duration (ms, 0 = sticky), action. Returns { dismiss }.
DS.openDialog(target) / DS.closeDialog(target, value)Open or close a <dialog> by selector or element.
DS.selectTab(tab)Select a tab and show its panel.
DS.syncRange(input?)Refresh the filled track of one range input, or all of them.
DS.versionLibrary version.

Data attributes

AttributeEffect
data-theme-value="light|dark|system"Click switches theme; gets aria-pressed.
data-ds-open="#dialog"Click opens that dialog.
data-ds-close="value"Click closes the surrounding dialog with that return value.
data-ds-dismissibleOn a dialog: close on backdrop click.
data-ds-dismissClick removes the surrounding .alert.
data-ds-toggle="dropdown"Toggle of a .dropdown.
data-ds-tabsOn a role="tablist": click and keyboard tab switching.

Events