Skip to content

Instantly share code, notes, and snippets.

@kristoferjoseph
Last active November 6, 2025 18:08
Show Gist options
  • Select an option

  • Save kristoferjoseph/04ae16317568c7d3f240b91cbd3273c0 to your computer and use it in GitHub Desktop.

Select an option

Save kristoferjoseph/04ae16317568c7d3f240b91cbd3273c0 to your computer and use it in GitHub Desktop.
layout components prototyping
<!DOCTYPE html>
<html lang="en" data-theme="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design System 808</title>
<style>
/* ============================================
PARAMETRIC DESIGN TOKENS
============================================ */
:root {
/* Enable native light/dark mode */
color-scheme: light dark;
/* Viewport Configuration */
--fluid-min-width: 320;
--fluid-max-width: 1500;
--fluid-screen: 100vw;
--fluid-bp: calc(
(var(--fluid-screen) - ((var(--fluid-min-width) / 16) * 1rem)) /
((var(--fluid-max-width) / 16) - (var(--fluid-min-width) / 16))
);
/* Fluid Type Scale (Minor Third → Perfect Fourth) */
--fluid--2: clamp(0.79rem, calc(0.77rem + 0.08vw), 0.84rem);
--fluid--1: clamp(0.89rem, calc(0.87rem + 0.11vw), 0.97rem);
--fluid-0: clamp(1.00rem, calc(0.98rem + 0.13vw), 1.13rem);
--fluid-1: clamp(1.20rem, calc(1.15rem + 0.25vw), 1.42rem);
--fluid-2: clamp(1.44rem, calc(1.35rem + 0.43vw), 1.78rem);
--fluid-3: clamp(1.73rem, calc(1.59rem + 0.67vw), 2.37rem);
--fluid-4: clamp(2.07rem, calc(1.88rem + 0.98vw), 3.16rem);
--fluid-5: clamp(2.49rem, calc(2.20rem + 1.43vw), 4.21rem);
/* Fluid Space Scale (Minor Third → Perfect Fourth) */
--space--2: clamp(0.50rem, calc(0.49rem + 0.07vw), 0.56rem);
--space--1: clamp(0.75rem, calc(0.73rem + 0.10vw), 0.84rem);
--space-0: clamp(1.00rem, calc(0.98rem + 0.13vw), 1.13rem);
--space-1: clamp(1.50rem, calc(1.46rem + 0.20vw), 1.69rem);
--space-2: clamp(2.00rem, calc(1.95rem + 0.27vw), 2.25rem);
--space-3: clamp(3.00rem, calc(2.93rem + 0.40vw), 3.38rem);
--space-4: clamp(4.00rem, calc(3.90rem + 0.54vw), 4.50rem);
--space-5: clamp(6.00rem, calc(5.85rem + 0.81vw), 6.75rem);
/* Border Radii */
--radius-1: 2px;
--radius-2: 4px;
--radius-3: 8px;
--radius-full: 9999px;
/* Border Widths */
--border-1: 1px;
--border-2: 2px;
/* Component-specific tokens */
--nav-width: 0px;
--rail-width: 0px;
--appbar-height: clamp(3.5rem, calc(3.41rem + 0.47vw), 3.94rem);
--footer-height: clamp(2.5rem, calc(2.44rem + 0.34vw), 2.81rem);
}
/* ============================================
LIGHT MODE (Default)
WCAG AA Compliant - 4.5:1 minimum contrast
============================================ */
:root,
:root[data-theme="light"] {
color-scheme: light;
/* Colorblind-safe blue (safe for protanopia/deuteranopia) */
accent-color: #0066CC;
--accent: #0066CC;
--canvas-base: #ffffff;
--canvas-elevated: #f5f5f5;
--canvas-inset: #e8e8e8;
/* WCAG AA compliant surfaces */
--surface-default: rgba(0, 0, 0, 0.08);
--surface-hover: rgba(0, 0, 0, 0.12);
--surface-active: rgba(0, 0, 0, 0.16);
/* WCAG AA compliant text - 4.5:1+ contrast on white */
--text-primary: #1a1a1a; /* 16.1:1 contrast */
--text-secondary: #4d4d4d; /* 8.6:1 contrast */
--text-tertiary: #737373; /* 4.6:1 contrast */
--border-default: rgba(0, 0, 0, 0.15);
/* Three-layer focus indicator */
--focus-ring-outer: #000000; /* Black outer ring */
--focus-ring-middle: #0066CC; /* Accent middle ring */
--focus-ring-inner: #ffffff; /* White inner ring */
}
/* ============================================
DARK MODE
WCAG AA Compliant - 4.5:1 minimum contrast
============================================ */
:root[data-theme="dark"] {
color-scheme: dark;
/* Lighter colorblind-safe blue for dark mode */
accent-color: #4D94FF;
--accent: #4D94FF;
--canvas-base: #0f0f0f;
--canvas-elevated: #1a1a1a;
--canvas-inset: #000000;
/* WCAG AA compliant surfaces */
--surface-default: rgba(255, 255, 255, 0.10);
--surface-hover: rgba(255, 255, 255, 0.15);
--surface-active: rgba(255, 255, 255, 0.20);
/* WCAG AA compliant text - 4.5:1+ contrast on #0f0f0f */
--text-primary: #f0f0f0; /* 15.3:1 contrast */
--text-secondary: #b3b3b3; /* 8.7:1 contrast */
--text-tertiary: #8c8c8c; /* 4.8:1 contrast */
--border-default: rgba(255, 255, 255, 0.20);
/* Three-layer focus indicator (reversed for dark mode) */
--focus-ring-outer: #ffffff; /* White outer ring */
--focus-ring-middle: #4D94FF; /* Accent middle ring */
--focus-ring-inner: #000000; /* Black inner ring */
}
/* Auto mode - follows system preference */
@media (prefers-color-scheme: dark) {
:root[data-theme="auto"] {
color-scheme: dark;
accent-color: #4D94FF;
--accent: #4D94FF;
--canvas-base: #0f0f0f;
--canvas-elevated: #1a1a1a;
--canvas-inset: #000000;
--surface-default: rgba(255, 255, 255, 0.10);
--surface-hover: rgba(255, 255, 255, 0.15);
--surface-active: rgba(255, 255, 255, 0.20);
--text-primary: #f0f0f0;
--text-secondary: #b3b3b3;
--text-tertiary: #8c8c8c;
--border-default: rgba(255, 255, 255, 0.20);
/* Three-layer focus indicator (reversed for dark mode) */
--focus-ring-outer: #ffffff;
--focus-ring-middle: #4D94FF;
--focus-ring-inner: #000000;
}
}
@media (min-width: 768px) {
:root {
--nav-width: 200px;
--rail-width: 280px;
}
}
/* Cap fluid scaling at max viewport */
@media screen and (min-width: 1500px) {
:root {
--fluid-screen: calc(var(--fluid-max-width) * 1px);
}
}
/* ============================================
BASE STYLES
============================================ */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, sans-serif;
margin: 0;
padding: 0;
background: var(--canvas-inset);
color: var(--text-primary);
font-size: var(--fluid-0);
line-height: 1.5;
height: 100vh;
transition: background 0.2s, color 0.2s;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
font-weight: 600;
color: currentColor;
}
h1 { font-size: var(--fluid-4); }
h2 { font-size: var(--fluid-3); }
h3 { font-size: var(--fluid-2); }
h4 { font-size: var(--fluid-1); }
h5 { font-size: var(--fluid-0); }
h6 { font-size: var(--fluid--1); }
.demo-container {
height: 100vh;
display: flex;
flex-direction: column;
}
button {
background-color: AccentColor;
color: AccentColorText;
border-radius: var(--radius-2);
padding: var(--space--1) var(--space-1);
border: 0;
cursor: pointer;
font-size: var(--fluid--1);
font-family: inherit;
line-height: 1;
transition: opacity 0.15s;
position: relative;
}
button:hover {
opacity: 0.9;
}
button:active {
opacity: 0.8;
}
button:focus-visible {
outline: 1px solid var(--focus-ring-outer);
outline-offset: 2px;
box-shadow:
0 0 0 1px var(--focus-ring-inner),
0 0 0 2px var(--focus-ring-middle);
}
/* Ensure minimum touch target size (44x44px) */
button {
min-height: 44px;
min-width: 44px;
}
code {
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
background: var(--surface-default);
padding: 0.125em 0.375em;
border-radius: var(--radius-1);
font-size: 0.9em;
}
/* Input base styles */
input[type="text"],
input[type="email"],
input[type="date"],
input[type="color"],
textarea,
select {
font-family: inherit;
font-size: var(--fluid--1);
color: var(--text-primary);
background: var(--canvas-base);
border: var(--border-1) solid var(--border-default);
border-radius: var(--radius-2);
padding: var(--space--1);
transition: border-color 0.15s, background 0.2s;
}
/* Native date picker styling */
input[type="date"] {
min-height: 44px;
cursor: pointer;
}
input[type="date"]::-webkit-calendar-picker-indicator {
cursor: pointer;
filter: var(--date-picker-filter, none);
}
/* Dark mode filter for calendar icon */
@media (prefers-color-scheme: dark) {
:root[data-theme="auto"] {
--date-picker-filter: invert(1);
}
}
:root[data-theme="dark"] {
--date-picker-filter: invert(1);
}
input[type="color"] {
width: 100px;
height: 40px;
padding: var(--space--2);
cursor: pointer;
}
input[type="text"]:focus,
input[type="email"]:focus,
input[type="date"]:focus,
textarea:focus,
select:focus {
outline: 1px solid var(--focus-ring-outer);
outline-offset: 2px;
box-shadow:
0 0 0 1px var(--focus-ring-inner),
0 0 0 2px var(--focus-ring-middle);
border-color: var(--focus-ring-middle);
}
/* Three-layer focus ring for all interactive elements */
*:focus-visible {
outline: 1px solid var(--focus-ring-outer);
outline-offset: 2px;
box-shadow:
0 0 0 1px var(--focus-ring-inner),
0 0 0 2px var(--focus-ring-middle);
}
/* Remove default outline, rely on focus-visible */
*:focus:not(:focus-visible) {
outline: none;
}
/* Accent color elements */
input[type="checkbox"],
input[type="radio"],
input[type="range"],
progress {
accent-color: inherit; /* Inherits from root */
}
input[type="checkbox"],
input[type="radio"] {
width: 18px;
height: 18px;
cursor: pointer;
}
input[type="range"] {
width: 100%;
cursor: pointer;
}
progress {
width: 100%;
height: 12px;
border: none;
border-radius: var(--radius-1);
background: var(--surface-default);
}
progress::-webkit-progress-bar {
background: var(--surface-default);
border-radius: var(--radius-1);
}
progress::-webkit-progress-value {
background: var(--accent);
border-radius: var(--radius-1);
}
progress::-moz-progress-bar {
background: var(--accent);
border-radius: var(--radius-1);
}
label {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
.checkbox-group,
.radio-group {
display: flex;
flex-direction: column;
gap: var(--space--2);
}
.checkbox-item,
.radio-item {
display: flex;
align-items: center;
gap: var(--space--1);
}
.checkbox-item label,
.radio-item label {
font-weight: normal;
cursor: pointer;
}
/* Table base styles */
table {
width: 100%;
border-collapse: collapse;
font-size: var(--fluid--1);
}
th, td {
text-align: left;
padding: var(--space--1) var(--space-0);
}
th {
font-weight: 600;
color: var(--text-primary);
}
td {
color: var(--text-secondary);
}
th button {
background: none;
border: none;
padding: 0;
font: inherit;
color: inherit;
cursor: pointer;
display: flex;
align-items: center;
gap: var(--space--2);
width: 100%;
text-align: left;
min-height: 44px;
}
th button:hover {
color: var(--text-primary);
}
th button:focus-visible {
outline: 1px solid var(--focus-ring-outer);
outline-offset: 2px;
box-shadow:
0 0 0 1px var(--focus-ring-inner),
0 0 0 2px var(--focus-ring-middle);
}
</style>
</head>
<body>
<div class="demo-container">
<ui-layout>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
}
::slotted([slot="header"]) { order: 1; }
::slotted([slot="leading"]) { order: 2; }
::slotted(:not([slot])) { order: 3; flex: 1; overflow-y: auto; }
::slotted([slot="trailing"]) { display: none; }
::slotted([slot="footer"]) { order: 4; }
@media (min-width: 768px) {
:host {
display: grid;
grid-template-columns: var(--nav-width) 1fr var(--rail-width);
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header header"
"nav content sidebar"
"footer footer footer";
}
::slotted([slot="header"]) { grid-area: header; order: unset; }
::slotted([slot="leading"]) { grid-area: nav; }
::slotted([slot="trailing"]) { display: flex; grid-area: sidebar; }
::slotted([slot="footer"]) { grid-area: footer; order: unset; }
::slotted(:not([slot])) { grid-area: content; order: unset; }
}
</style>
<slot name="header"></slot>
<slot name="leading"></slot>
<slot></slot>
<slot name="trailing"></slot>
<slot name="footer"></slot>
</template>
<ui-appbar slot="header" title="Design System">
<template shadowrootmode="open">
<style>
:host {
display: block;
background: var(--canvas-base);
border-bottom: var(--border-1) solid var(--border-default);
transition: background 0.2s;
}
.appbar {
display: flex;
align-items: center;
min-height: var(--appbar-height);
padding: 0 var(--space-1);
gap: var(--space-1);
}
.title {
font-size: var(--fluid-1);
font-weight: 600;
flex: 1;
}
.actions {
display: flex;
gap: var(--space--1);
align-items: center;
}
::slotted(div) {
display: flex;
gap: var(--space--1);
align-items: center;
}
@media (min-width: 768px) {
.appbar { padding: 0 var(--space-2); }
}
</style>
<div class="appbar">
<div class="title"></div>
<div class="actions">
<slot name="actions"></slot>
</div>
</div>
</template>
<div slot="actions">
<ui-theme-switcher>
<template shadowrootmode="open">
<style>
:host {
display: inline-flex;
gap: 2px;
background: var(--surface-default);
padding: 2px;
border-radius: var(--radius-2);
}
button {
background: transparent;
border: none;
padding: var(--space--2) var(--space--1);
border-radius: var(--radius-1);
cursor: pointer;
font-size: var(--fluid--2);
color: var(--text-secondary);
transition: all 0.15s;
font-family: inherit;
min-width: 44px;
min-height: 44px;
}
button:hover {
background: var(--surface-hover);
color: var(--text-primary);
}
button.active {
background: AccentColor;
color: AccentColorText;
}
button:focus-visible {
outline: 1px solid var(--focus-ring-outer);
outline-offset: 2px;
box-shadow:
0 0 0 1px var(--focus-ring-inner),
0 0 0 2px var(--focus-ring-middle);
}
</style>
<button data-theme="light" aria-label="Switch to light theme" title="Light theme">+</button>
<button data-theme="auto" class="active" aria-label="Use system theme preference" title="Auto theme">=</button>
<button data-theme="dark" aria-label="Switch to dark theme" title="Dark theme">-</button>
</template>
</ui-theme-switcher>
<ui-divider orientation="vertical">
<template shadowrootmode="open">
<style>
:host {
display: inline-block;
width: var(--border-1);
height: 24px;
background: var(--border-default);
margin: 0 var(--space--1);
}
</style>
</template>
</ui-divider>
<button>Save</button>
<button>Publish</button>
</div>
</ui-appbar>
<ui-navigation slot="leading">
<template shadowrootmode="open">
<style>
:host {
display: block;
background: var(--canvas-elevated);
border-bottom: var(--border-1) solid var(--border-default);
transition: background 0.2s;
}
@media (min-width: 768px) {
:host {
width: var(--nav-width);
height: 100%;
border-bottom: none;
border-right: var(--border-1) solid var(--border-default);
overflow-y: auto;
}
}
</style>
<nav aria-label="Main navigation">
<slot></slot>
</nav>
</template>
<ui-nav-menu>
<template shadowrootmode="open">
<style>
:host {
display: flex;
gap: var(--space--1);
padding: var(--space--1) var(--space-1);
overflow-x: auto;
overflow-y: hidden;
}
@media (min-width: 768px) {
:host {
display: block;
padding: var(--space--1);
overflow-x: visible;
}
}
</style>
<slot></slot>
</template>
<ui-nav-item>
<template shadowrootmode="open">
<style>
:host {
display: block;
flex-shrink: 0;
}
.nav-item {
display: block;
padding: var(--space--1) var(--space-0);
border-radius: var(--radius-2);
cursor: pointer;
color: var(--text-secondary);
transition: all 0.15s;
white-space: nowrap;
font-size: var(--fluid--1);
background: transparent;
text-decoration: none;
min-height: 44px;
display: flex;
align-items: center;
}
.nav-item:hover {
background: var(--surface-hover);
color: var(--text-primary);
}
.nav-item.active {
background: AccentColor;
color: AccentColorText;
}
.nav-item:focus-visible {
outline: 1px solid var(--focus-ring-outer);
outline-offset: 2px;
box-shadow:
0 0 0 1px var(--focus-ring-inner),
0 0 0 2px var(--focus-ring-middle);
}
@media (min-width: 768px) {
.nav-item {
margin-bottom: 2px;
}
}
</style>
<a href="#dashboard" class="nav-item" role="button" tabindex="0">
<slot></slot>
</a>
</template>
Dashboard
</ui-nav-item>
<ui-nav-item active>
<template shadowrootmode="open">
<style>
:host {
display: block;
flex-shrink: 0;
}
.nav-item {
display: block;
padding: var(--space--1) var(--space-0);
border-radius: var(--radius-2);
cursor: pointer;
color: var(--text-secondary);
transition: all 0.15s;
white-space: nowrap;
font-size: var(--fluid--1);
background: transparent;
text-decoration: none;
min-height: 44px;
display: flex;
align-items: center;
}
.nav-item:hover {
background: var(--surface-hover);
color: var(--text-primary);
}
.nav-item.active {
background: AccentColor;
color: AccentColorText;
}
.nav-item:focus-visible {
outline: 3px solid var(--focus-ring);
outline-offset: 2px;
}
@media (min-width: 768px) {
.nav-item {
margin-bottom: 2px;
}
}
</style>
<a href="#documents" class="nav-item active" role="button" tabindex="0" aria-current="page">
<slot></slot>
</a>
</template>
Documents
</ui-nav-item>
<ui-nav-item>
<template shadowrootmode="open">
<style>
:host {
display: block;
flex-shrink: 0;
}
.nav-item {
display: block;
padding: var(--space--1) var(--space-0);
border-radius: var(--radius-2);
cursor: pointer;
color: var(--text-secondary);
transition: all 0.15s;
white-space: nowrap;
font-size: var(--fluid--1);
background: transparent;
text-decoration: none;
min-height: 44px;
display: flex;
align-items: center;
}
.nav-item:hover {
background: var(--surface-hover);
color: var(--text-primary);
}
.nav-item.active {
background: AccentColor;
color: AccentColorText;
}
.nav-item:focus-visible {
outline: 3px solid var(--focus-ring);
outline-offset: 2px;
}
@media (min-width: 768px) {
.nav-item {
margin-bottom: 2px;
}
}
</style>
<a href="#media" class="nav-item" role="button" tabindex="0">
<slot></slot>
</a>
</template>
Media
</ui-nav-item>
<ui-nav-item>
<template shadowrootmode="open">
<style>
:host {
display: block;
flex-shrink: 0;
}
.nav-item {
display: block;
padding: var(--space--1) var(--space-0);
border-radius: var(--radius-2);
cursor: pointer;
color: var(--text-secondary);
transition: all 0.15s;
white-space: nowrap;
font-size: var(--fluid--1);
background: transparent;
text-decoration: none;
min-height: 44px;
display: flex;
align-items: center;
}
.nav-item:hover {
background: var(--surface-hover);
color: var(--text-primary);
}
.nav-item.active {
background: AccentColor;
color: AccentColorText;
}
.nav-item:focus-visible {
outline: 3px solid var(--focus-ring);
outline-offset: 2px;
}
@media (min-width: 768px) {
.nav-item {
margin-bottom: 2px;
}
}
</style>
<a href="#settings" class="nav-item" role="button" tabindex="0">
<slot></slot>
</a>
</template>
Settings
</ui-nav-item>
</ui-nav-menu>
</ui-navigation>
<ui-content>
<template shadowrootmode="open">
<style>
:host {
display: block;
width: 100%;
height: 100%;
overflow-y: auto;
background: var(--canvas-base);
transition: background 0.2s;
}
</style>
<main role="main">
<slot></slot>
</main>
</template>
<ui-section>
<template shadowrootmode="open">
<style>
:host {
display: block;
padding: var(--space-1);
border-bottom: var(--border-1) solid var(--border-default);
}
:host(:last-child) {
border-bottom: none;
}
::slotted(h1), ::slotted(h2), ::slotted(h3),
::slotted(h4), ::slotted(h5), ::slotted(h6) {
margin-top: 0;
margin-bottom: var(--space-0);
}
::slotted(p) {
margin: 0 0 var(--space-0) 0;
color: var(--text-secondary);
line-height: 1.6;
}
::slotted(p:last-child) {
margin-bottom: 0;
}
@media (min-width: 768px) {
:host {
padding: var(--space-2);
}
}
</style>
<slot></slot>
</template>
<h2>Theme Controls</h2>
<p>Customize the appearance of the design system using the controls below.</p>
<ui-input-group>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
gap: var(--space--2);
margin-bottom: var(--space-1);
}
::slotted(label) {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
::slotted(input),
::slotted(textarea),
::slotted(select) {
width: 100%;
}
::slotted(span) {
font-size: var(--fluid--2);
color: var(--text-secondary);
}
</style>
<slot></slot>
</template>
<label for="accent-picker">Accent Color</label>
<input type="color" id="accent-picker" value="#2563eb">
<span>Changes all interactive elements instantly</span>
</ui-input-group>
</ui-section>
<ui-section>
<template shadowrootmode="open">
<style>
:host {
display: block;
padding: var(--space-1);
border-bottom: var(--border-1) solid var(--border-default);
}
:host(:last-child) {
border-bottom: none;
}
::slotted(h1), ::slotted(h2), ::slotted(h3),
::slotted(h4), ::slotted(h5), ::slotted(h6) {
margin-top: 0;
margin-bottom: var(--space-0);
}
::slotted(p) {
margin: 0 0 var(--space-0) 0;
color: var(--text-secondary);
line-height: 1.6;
}
::slotted(p:last-child) {
margin-bottom: 0;
}
@media (min-width: 768px) {
:host {
padding: var(--space-2);
}
}
</style>
<slot></slot>
</template>
<h3>Form Components</h3>
<p>All form elements automatically inherit the accent color from the theme.</p>
<ui-input-group>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
gap: var(--space--2);
margin-bottom: var(--space-1);
}
::slotted(label) {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
::slotted(input),
::slotted(textarea),
::slotted(select) {
width: 100%;
}
::slotted(span) {
font-size: var(--fluid--2);
color: var(--text-secondary);
}
</style>
<slot></slot>
</template>
<label for="name-input">Name</label>
<input type="text" id="name-input" placeholder="Enter your name" aria-describedby="name-input-help">
<span id="name-input-help">Text inputs use accent color for focus outline</span>
</ui-input-group>
<ui-input-group>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
gap: var(--space--2);
margin-bottom: var(--space-1);
}
::slotted(label) {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
::slotted(input),
::slotted(textarea),
::slotted(select) {
width: 100%;
}
::slotted(span) {
font-size: var(--fluid--2);
color: var(--text-secondary);
}
</style>
<slot></slot>
</template>
<label for="category-select">Category</label>
<select id="category-select" aria-describedby="category-select-help">
<option>Design</option>
<option>Development</option>
<option>Marketing</option>
<option>Sales</option>
</select>
<span id="category-select-help">Select dropdowns use accent color</span>
</ui-input-group>
<ui-input-group>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
gap: var(--space--2);
margin-bottom: var(--space-1);
}
::slotted(label) {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
::slotted(input),
::slotted(textarea),
::slotted(select) {
width: 100%;
}
::slotted(span) {
font-size: var(--fluid--2);
color: var(--text-secondary);
}
</style>
<slot></slot>
</template>
<label for="volume-range">Volume</label>
<input type="range" id="volume-range" min="0" max="100" value="75" aria-describedby="volume-range-help" aria-valuemin="0" aria-valuemax="100" aria-valuenow="75">
<span id="volume-range-help">Range sliders use accent color for the track fill</span>
</ui-input-group>
<ui-input-group>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
gap: var(--space--2);
margin-bottom: var(--space-1);
}
::slotted(label) {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
::slotted(input),
::slotted(textarea),
::slotted(select) {
width: 100%;
}
::slotted(span) {
font-size: var(--fluid--2);
color: var(--text-secondary);
}
::slotted(.checkbox-group),
::slotted(.radio-group) {
display: flex;
flex-direction: column;
gap: var(--space--2);
}
::slotted(.checkbox-item),
::slotted(.radio-item) {
display: flex;
align-items: center;
gap: var(--space--1);
}
::slotted(input[type="checkbox"]),
::slotted(input[type="radio"]) {
width: auto;
cursor: pointer;
}
</style>
<slot></slot>
</template>
<label id="preferences-label">Preferences</label>
<div class="checkbox-group" role="group" aria-labelledby="preferences-label">
<div class="checkbox-item">
<input type="checkbox" id="newsletter" checked>
<label for="newsletter" style="font-weight: normal; cursor: pointer;">Subscribe to newsletter</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="updates" checked>
<label for="updates" style="font-weight: normal; cursor: pointer;">Receive product updates</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="offers">
<label for="offers" style="font-weight: normal; cursor: pointer;">Special offers</label>
</div>
</div>
<span id="preferences-help">Checkboxes use accent color when checked</span>
</ui-input-group>
<ui-input-group>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
gap: var(--space--2);
margin-bottom: var(--space-1);
}
::slotted(label) {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
::slotted(input),
::slotted(textarea),
::slotted(select) {
width: 100%;
}
::slotted(span) {
font-size: var(--fluid--2);
color: var(--text-secondary);
}
::slotted(.checkbox-group),
::slotted(.radio-group) {
display: flex;
flex-direction: column;
gap: var(--space--2);
}
::slotted(.checkbox-item),
::slotted(.radio-item) {
display: flex;
align-items: center;
gap: var(--space--1);
}
::slotted(input[type="checkbox"]),
::slotted(input[type="radio"]) {
width: auto;
cursor: pointer;
}
</style>
<slot></slot>
</template>
<label id="plan-label">Plan Type</label>
<div class="radio-group" role="radiogroup" aria-labelledby="plan-label" aria-describedby="plan-help">
<div class="radio-item">
<input type="radio" id="plan-basic" name="plan" value="basic">
<label for="plan-basic" style="font-weight: normal; cursor: pointer;">Basic</label>
</div>
<div class="radio-item">
<input type="radio" id="plan-pro" name="plan" value="pro" checked>
<label for="plan-pro" style="font-weight: normal; cursor: pointer;">Pro</label>
</div>
<div class="radio-item">
<input type="radio" id="plan-enterprise" name="plan" value="enterprise">
<label for="plan-enterprise" style="font-weight: normal; cursor: pointer;">Enterprise</label>
</div>
</div>
<span id="plan-help">Radio buttons use accent color when selected</span>
</ui-input-group>
<ui-input-group>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
gap: var(--space--2);
margin-bottom: var(--space-1);
}
::slotted(label) {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
::slotted(input),
::slotted(textarea),
::slotted(select) {
width: 100%;
}
::slotted(span) {
font-size: var(--fluid--2);
color: var(--text-secondary);
}
::slotted(progress) {
width: 100%;
height: 12px;
}
</style>
<slot></slot>
</template>
<label for="upload-progress">Upload Progress</label>
<progress id="upload-progress" value="65" max="100" aria-describedby="upload-progress-help">65%</progress>
<span id="upload-progress-help">Progress bars use accent color for completion</span>
</ui-input-group>
<ui-button-stack>
<template shadowrootmode="open">
<style>
:host {
display: flex;
gap: var(--space--1);
align-items: center;
flex-wrap: wrap;
margin-top: var(--space-1);
}
</style>
<slot></slot>
</template>
<button>Submit</button>
<button style="background: var(--surface-default); color: var(--text-primary);">Cancel</button>
</ui-button-stack>
</ui-section>
<ui-section>
<template shadowrootmode="open">
<style>
:host {
display: block;
padding: var(--space-1);
border-bottom: var(--border-1) solid var(--border-default);
}
:host(:last-child) {
border-bottom: none;
}
::slotted(h1), ::slotted(h2), ::slotted(h3),
::slotted(h4), ::slotted(h5), ::slotted(h6) {
margin-top: 0;
margin-bottom: var(--space-0);
}
::slotted(p) {
margin: 0 0 var(--space-0) 0;
color: var(--text-secondary);
line-height: 1.6;
}
::slotted(p:last-child) {
margin-bottom: 0;
}
@media (min-width: 768px) {
:host {
padding: var(--space-2);
}
}
</style>
<slot></slot>
</template>
<h3>Date Picker Components</h3>
<p>Native browser date picker styled with theme tokens, and a custom calendar component using the Temporal API for range selection.</p>
<ui-input-group>
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
gap: var(--space--2);
margin-bottom: var(--space-1);
}
::slotted(label) {
font-size: var(--fluid--1);
font-weight: 500;
color: var(--text-primary);
}
::slotted(input),
::slotted(textarea),
::slotted(select) {
width: 100%;
}
::slotted(span) {
font-size: var(--fluid--2);
color: var(--text-secondary);
}
</style>
<slot></slot>
</template>
<label for="native-date">Native Date Picker</label>
<input type="date" id="native-date" aria-describedby="native-date-help">
<span id="native-date-help">Uses browser's native date picker with theme styling</span>
</ui-input-group>
<ui-divider>
<template shadowrootmode="open">
<style>
:host {
display: block;
width: 100%;
height: var(--border-1);
background: var(--border-default);
margin: var(--space-1) 0;
}
</style>
</template>
</ui-divider>
<h4>Custom Range Calendar (Temporal API)</h4>
<p style="font-size: var(--fluid--1); color: var(--text-secondary); margin-bottom: var(--space-1);">
Select a date range like Airbnb. Click start date, then end date.
</p>
<ui-date-range-picker>
<template shadowrootmode="open">
<style>
:host {
display: block;
margin-top: var(--space-1);
}
.calendar-container {
border: var(--border-1) solid var(--border-default);
border-radius: var(--radius-2);
background: var(--canvas-base);
padding: var(--space-1);
max-width: 320px;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-0);
padding-bottom: var(--space-0);
border-bottom: var(--border-1) solid var(--border-default);
}
.month-year {
font-weight: 600;
font-size: var(--fluid-0);
color: var(--text-primary);
}
.nav-button {
background: var(--surface-default);
border: none;
border-radius: var(--radius-2);
width: 32px;
height: 32px;
cursor: pointer;
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--fluid-0);
transition: background 0.15s;
}
.nav-button:hover {
background: var(--surface-hover);
}
.nav-button:focus-visible {
outline: 1px solid var(--focus-ring-outer);
outline-offset: 2px;
box-shadow:
0 0 0 1px var(--focus-ring-inner),
0 0 0 2px var(--focus-ring-middle);
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-top: var(--space-0);
}
.day-label {
text-align: center;
font-size: var(--fluid--2);
font-weight: 600;
color: var(--text-secondary);
padding: var(--space--2);
}
.day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--fluid--1);
color: var(--text-secondary);
border-radius: var(--radius-2);
cursor: pointer;
background: transparent;
border: var(--border-1) solid transparent;
transition: all 0.15s;
min-width: 36px;
min-height: 36px;
}
.day:hover:not(.disabled):not(.selected-start):not(.selected-end) {
background: var(--surface-hover);
color: var(--text-primary);
}
.day.disabled {
color: var(--text-tertiary);
cursor: not-allowed;
opacity: 0.4;
}
.day.other-month {
color: var(--text-tertiary);
}
.day.today {
border-color: var(--border-default);
font-weight: 600;
}
.day.in-range {
background: var(--surface-default);
}
.day.selected-start,
.day.selected-end {
background: AccentColor;
color: AccentColorText;
font-weight: 600;
}
.day:focus-visible {
outline: 1px solid var(--focus-ring-outer);
outline-offset: 2px;
box-shadow:
0 0 0 1px var(--focus-ring-inner),
0 0 0 2px var(--focus-ring-middle);
}
.selected-range {
margin-top: var(--space-1);
padding: var(--space-0);
background: var(--surface-default);
border-radius: var(--radius-2);
font-size: var(--fluid--1);
color: var(--text-secondary);
}
.selected-range strong {
color: var(--text-primary);
font-weight: 600;
}
.clear-button {
background: var(--surface-default);
color: var(--text-primary);
border: none;
padding: var(--space--2) var(--space-0);
border-radius: var(--radius-2);
cursor: pointer;
font-size: var(--fluid--2);
margin-top: var(--space--1);
width: 100%;
transition: background 0.15s;
}
.clear-button:hover {
background: var(--surface-hover);
}
</style>
<div class="calendar-container" role="application" aria-label="Date range picker calendar">
<div class="calendar-header">
<button class="nav-button" type="button" aria-label="Previous month">‹</button>
<div class="month-year" aria-live="polite"></div>
<button class="nav-button" type="button" aria-label="Next month">›</button>
</div>
<div class="calendar-grid" role="grid" aria-label="Calendar dates">
<!-- Day labels -->
<div class="day-label" role="columnheader" aria-label="Sunday">Su</div>
<div class="day-label" role="columnheader" aria-label="Monday">Mo</div>
<div class="day-label" role="columnheader" aria-label="Tuesday">Tu</div>
<div class="day-label" role="columnheader" aria-label="Wednesday">We</div>
<div class="day-label" role="columnheader" aria-label="Thursday">Th</div>
<div class="day-label" role="columnheader" aria-label="Friday">Fr</div>
<div class="day-label" role="columnheader" aria-label="Saturday">Sa</div>
<!-- Days will be inserted here -->
</div>
<div class="selected-range" role="status" aria-live="polite"></div>
<button class="clear-button" type="button" style="display: none;">Clear Selection</button>
</div>
</template>
</ui-date-range-picker>
<p style="margin-top: var(--space-1); font-size: var(--fluid--2); color: var(--text-secondary);">
<strong>Temporal API Features:</strong> Accurate date calculations, timezone-aware, range selection, keyboard navigation (Arrow keys), proper month/year handling, and accessible date announcements.
</p>
</ui-section>
<ui-section>
<template shadowrootmode="open">
<style>
:host {
display: block;
padding: var(--space-1);
border-bottom: var(--border-1) solid var(--border-default);
}
:host(:last-child) {
border-bottom: none;
}
::slotted(h1), ::slotted(h2), ::slotted(h3),
::slotted(h4), ::slotted(h5), ::slotted(h6) {
margin-top: 0;
margin-bottom: var(--space-0);
}
::slotted(p) {
margin: 0 0 var(--space-0) 0;
color: var(--text-secondary);
line-height: 1.6;
}
::slotted(p:last-child) {
margin-bottom: 0;
}
@media (min-width: 768px) {
:host {
padding: var(--space-2);
}
}
</style>
<slot></slot>
</template>
<h3>Grid-Based Data Table</h3>
<p>Inspired by TanStack Table, built with CSS Grid for maximum flexibility and control. Features text truncation with tooltips, sortable columns, and maintains grid integrity.</p>
<ui-grid-table columns="40px 1fr 2fr 1fr 100px">
<template shadowrootmode="open">
<style>
:host {
display: block;
width: 100%;
overflow-x: auto;
margin-top: var(--space-1);
}
.grid-table {
display: grid;
grid-template-columns: var(--columns, 1fr 1fr 1fr);
border: var(--border-1) solid var(--border-default);
border-radius: var(--radius-2);
overflow: hidden;
background: var(--canvas-base);
}
/* All cells get base styling */
::slotted(.header-cell),
::slotted(.cell) {
padding: var(--space-0);
border-right: var(--border-1) solid var(--border-default);
border-bottom: var(--border-1) solid var(--border-default);
display: flex;
align-items: center;
min-height: 44px;
overflow: hidden;
}
/* Header cells */
::slotted(.header-cell) {
background: var(--canvas-elevated);
font-weight: 600;
color: var(--text-primary);
font-size: var(--fluid--1);
border-bottom: var(--border-2) solid var(--border-default);
position: sticky;
top: 0;
z-index: 10;
}
/* Body cells */
::slotted(.cell) {
color: var(--text-secondary);
font-size: var(--fluid--1);
background: var(--canvas-base);
}
/* Remove right border from last column */
::slotted(.header-cell:nth-child(5n)),
::slotted(.cell:nth-child(5n)) {
border-right: none;
}
/* Remove bottom border from last row */
::slotted(.cell:nth-last-child(-n+5)) {
border-bottom: none;
}
/* Row hover - target every 5th element starting from position based on column count */
::slotted(.cell:hover),
::slotted(.cell:nth-child(5n+6):hover) ~ ::slotted(.cell:nth-child(-n+10)),
::slotted(.cell:nth-child(5n+7):hover) ~ ::slotted(.cell:nth-child(-n+10)),
::slotted(.cell:nth-child(5n+8):hover) ~ ::slotted(.cell:nth-child(-n+10)),
::slotted(.cell:nth-child(5n+9):hover) ~ ::slotted(.cell:nth-child(-n+10)),
::slotted(.cell:nth-child(5n+10):hover) ~ ::slotted(.cell:nth-child(-n+10)) {
background: var(--surface-hover);
}
/* Alternating row backgrounds */
::slotted(.cell:nth-child(10n+6)),
::slotted(.cell:nth-child(10n+7)),
::slotted(.cell:nth-child(10n+8)),
::slotted(.cell:nth-child(10n+9)),
::slotted(.cell:nth-child(10n+10)) {
background: var(--surface-default);
}
</style>
<div class="grid-table">
<slot></slot>
</div>
</template>
<!-- Header Row -->
<div class="header-cell">
<input type="checkbox" id="grid-select-all" aria-label="Select all rows">
</div>
<div class="header-cell">
<button class="sort-button" type="button" aria-sort="ascending" aria-label="Sort by name" title="Click to sort by Name" style="background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; display: flex; align-items: center; gap: var(--space--2); width: 100%; min-height: 44px;">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">Name</span>
<span aria-hidden="true" style="flex-shrink: 0;">↑</span>
</button>
</div>
<div class="header-cell">
<button class="sort-button" type="button" aria-sort="none" aria-label="Sort by email" title="Click to sort by Email" style="background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; display: flex; align-items: center; gap: var(--space--2); width: 100%; min-height: 44px;">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">Email Address</span>
<span aria-hidden="true" style="flex-shrink: 0; opacity: 0.3;">↕</span>
</button>
</div>
<div class="header-cell">
<button class="sort-button" type="button" aria-sort="none" aria-label="Sort by role" title="Click to sort by Role" style="background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; display: flex; align-items: center; gap: var(--space--2); width: 100%; min-height: 44px;">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">Role</span>
<span aria-hidden="true" style="flex-shrink: 0; opacity: 0.3;">↕</span>
</button>
</div>
<div class="header-cell">
<button class="sort-button" type="button" aria-sort="none" aria-label="Sort by status" title="Click to sort by Status" style="background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; display: flex; align-items: center; gap: var(--space--2); width: 100%; min-height: 44px;">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">Status</span>
<span aria-hidden="true" style="flex-shrink: 0; opacity: 0.3;">↕</span>
</button>
</div>
<!-- Row 1 -->
<div class="cell">
<input type="checkbox" aria-label="Select Alice Johnson">
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Alice Johnson">Alice Johnson</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Senior Product Designer">Senior Product Designer</span>
</div>
<div class="cell">
<span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-active); border-radius: var(--radius-1); font-size: var(--fluid--2);" title="Active user">Active</span>
</div>
<!-- Row 2 -->
<div class="cell">
<input type="checkbox" aria-label="Select Bob Smith">
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Bob Smith">Bob Smith</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Full Stack Developer">Full Stack Developer</span>
</div>
<div class="cell">
<span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-active); border-radius: var(--radius-1); font-size: var(--fluid--2);" title="Active user">Active</span>
</div>
<!-- Row 3 -->
<div class="cell">
<input type="checkbox" aria-label="Select Carol White">
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Carol White">Carol White</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Engineering Manager">Engineering Manager</span>
</div>
<div class="cell">
<span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-default); border-radius: var(--radius-1); font-size: var(--fluid--2); color: var(--text-tertiary);" title="Inactive user">Inactive</span>
</div>
<!-- Row 4 -->
<div class="cell">
<input type="checkbox" aria-label="Select David Brown">
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="David Brown">David Brown</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Backend Developer">Backend Developer</span>
</div>
<div class="cell">
<span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-active); border-radius: var(--radius-1); font-size: var(--fluid--2);" title="Active user">Active</span>
</div>
<!-- Row 5 -->
<div class="cell">
<input type="checkbox" aria-label="Select Emma Davis">
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Emma Davis">Emma Davis</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span>
</div>
<div class="cell">
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="UX/UI Designer with specialization in accessibility">UX/UI Designer with specialization in accessibility</span>
</div>
<div class="cell">
<span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-active); border-radius: var(--radius-1); font-size: var(--fluid--2);" title="Active user">Active</span>
</div>
</ui-grid-table>
<p style="margin-top: var(--space-1); font-size: var(--fluid--2); color: var(--text-secondary);">
<strong>CSS Grid Features:</strong> Custom column sizing (40px 1fr 2fr 1fr 100px), text truncation with tooltips, sticky headers, keyboard navigation, and dividers that maintain grid integrity. Hover over truncated text to see full content.
</p>
</ui-section>
<ui-section>
<template shadowrootmode="open">
<style>
:host {
display: block;
padding: var(--space-1);
border-bottom: var(--border-1) solid var(--border-default);
}
:host(:last-child) {
border-bottom: none;
}
::slotted(h1), ::slotted(h2), ::slotted(h3),
::slotted(h4), ::slotted(h5), ::slotted(h6) {
margin-top: 0;
margin-bottom: var(--space-0);
}
::slotted(p) {
margin: 0 0 var(--space-0) 0;
color: var(--text-secondary);
line-height: 1.6;
}
::slotted(p:last-child) {
margin-bottom: 0;
}
@media (min-width: 768px) {
:host {
padding: var(--space-2);
}
}
</style>
<slot></slot>
</template>
<h3>Layout Utilities</h3>
<p>Dividers and spacing components for organizing content.</p>
<p>Horizontal divider below:</p>
<ui-divider>
<template shadowrootmode="open">
<style>
:host {
display: block;
width: 100%;
height: var(--border-1);
background: var(--border-default);
margin: var(--space-1) 0;
}
</style>
</template>
</ui-divider>
<p>Content continues after divider</p>
</ui-section>
</ui-content>
<ui-rail slot="trailing">
<template shadowrootmode="open">
<style>
:host {
display: flex;
flex-direction: column;
width: var(--rail-width);
height: 100%;
border-left: var(--border-1) solid var(--border-default);
overflow-y: auto;
background: var(--canvas-elevated);
transition: background 0.2s;
}
</style>
<aside role="complementary" aria-label="Sidebar information">
<slot></slot>
</aside>
</template>
<ui-section>
<template shadowrootmode="open">
<style>
:host {
display: block;
padding: var(--space-1);
border-bottom: var(--border-1) solid var(--border-default);
}
:host(:last-child) {
border-bottom: none;
}
::slotted(h1), ::slotted(h2), ::slotted(h3),
::slotted(h4), ::slotted(h5), ::slotted(h6) {
margin-top: 0;
margin-bottom: var(--space-0);
}
::slotted(p) {
margin: 0 0 var(--space-0) 0;
color: var(--text-secondary);
line-height: 1.6;
}
::slotted(p:last-child) {
margin-bottom: 0;
}
@media (min-width: 768px) {
:host {
padding: var(--space-2);
}
}
</style>
<slot></slot>
</template>
<h4>Accessibility</h4>
<p>WCAG AA compliant design:</p>
<ul style="margin: var(--space-0) 0 0 0; padding-left: var(--space-1); color: var(--text-secondary); font-size: var(--fluid--2);">
<li style="margin-bottom: var(--space--2);">4.5:1+ contrast ratios</li>
<li style="margin-bottom: var(--space--2);">Colorblind-safe palette</li>
<li style="margin-bottom: var(--space--2);">Proper ARIA labels</li>
<li style="margin-bottom: var(--space--2);">Keyboard navigation</li>
<li style="margin-bottom: var(--space--2);">44px touch targets</li>
<li style="margin-bottom: var(--space--2);">3-layer focus ring</li>
<li style="margin-bottom: var(--space--2);">High contrast indicators</li>
<li style="margin-bottom: var(--space--2);">CSS Grid data tables</li>
<li style="margin-bottom: var(--space--2);">Date range selection</li>
<li style="margin-bottom: var(--space--2);">Text truncation tooltips</li>
<li style="margin-bottom: var(--space--2);">Screen reader support</li>
</ul>
<ui-divider>
<template shadowrootmode="open">
<style>
:host {
display: block;
width: 100%;
height: var(--border-1);
background: var(--border-default);
margin: var(--space-1) 0;
}
</style>
</template>
</ui-divider>
<h4>Theme System</h4>
<p>Built on CSS system colors:</p>
<ul style="margin: var(--space-0) 0 0 0; padding-left: var(--space-1); color: var(--text-secondary);">
<li style="margin-bottom: var(--space--2);"><code>accent-color</code></li>
<li style="margin-bottom: var(--space--2);"><code>AccentColor</code></li>
<li style="margin-bottom: var(--space--2);"><code>AccentColorText</code></li>
<li style="margin-bottom: var(--space--2);"><code>currentColor</code></li>
</ul>
<ui-divider>
<template shadowrootmode="open">
<style>
:host {
display: block;
width: 100%;
height: var(--border-1);
background: var(--border-default);
margin: var(--space-1) 0;
}
</style>
</template>
</ui-divider>
<h5 style="font-size: var(--fluid-0); margin-bottom: var(--space--1);">Accent Color Applies To:</h5>
<ul style="margin: 0; padding-left: var(--space-1); color: var(--text-secondary); font-size: var(--fluid--2);">
<li style="margin-bottom: var(--space--2);">Checkboxes ✓</li>
<li style="margin-bottom: var(--space--2);">Radio buttons ✓</li>
<li style="margin-bottom: var(--space--2);">Range sliders ✓</li>
<li style="margin-bottom: var(--space--2);">Progress bars ✓</li>
<li style="margin-bottom: var(--space--2);">Select dropdowns ✓</li>
<li style="margin-bottom: var(--space--2);">Date pickers ✓</li>
<li style="margin-bottom: var(--space--2);">Buttons (custom) ✓</li>
<li style="margin-bottom: var(--space--2);">Active states ✓</li>
<li style="margin-bottom: var(--space--2);">Focus outlines ✓</li>
<li style="margin-bottom: var(--space--2);">Grid table sorting ✓</li>
</ul>
</ui-section>
</ui-rail>
<ui-footer slot="footer" status="WCAG AA Compliant • Colorblind-Safe • Keyboard Accessible">
<template shadowrootmode="open">
<style>
:host {
display: block;
background: var(--canvas-elevated);
border-top: var(--border-1) solid var(--border-default);
transition: background 0.2s;
}
.footer {
display: flex;
align-items: center;
min-height: var(--footer-height);
padding: 0 var(--space-1);
font-size: var(--fluid--2);
color: var(--text-tertiary);
}
@media (min-width: 768px) {
.footer { padding: 0 var(--space-2); }
}
</style>
<footer role="contentinfo" class="footer"></footer>
</template>
</ui-footer>
</ui-layout>
</div>
<script>
// ============================================
// THEME MANAGEMENT
// ============================================
function initTheme() {
const saved = localStorage.getItem('theme') || 'auto';
document.documentElement.setAttribute('data-theme', saved);
}
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
// Update all theme switcher buttons
document.querySelectorAll('ui-theme-switcher').forEach(switcher => {
const buttons = switcher.shadowRoot.querySelectorAll('button');
buttons.forEach(btn => {
const btnTheme = btn.getAttribute('data-theme');
btn.classList.toggle('active', btnTheme === theme);
});
});
}
// Initialize theme on load
initTheme();
// Set up theme switcher and accent color picker after DOM loads
document.addEventListener('DOMContentLoaded', () => {
// Accent color picker
const picker = document.getElementById('accent-picker');
if (picker) {
picker.addEventListener('input', (e) => {
const color = e.target.value;
document.documentElement.style.setProperty('accent-color', color);
document.documentElement.style.setProperty('--accent', color);
document.documentElement.style.setProperty('--focus-ring-middle', color);
});
}
});
// ============================================
// COMPONENT DEFINITIONS
// All use Declarative Shadow DOM
// JavaScript only for attribute observation
// ============================================
class UiLayout extends HTMLElement {
constructor() { super(); }
}
class UiAppBar extends HTMLElement {
static observedAttributes = ["title"];
constructor() { super(); }
connectedCallback() { this.updateTitle(); }
attributeChangedCallback(name, oldValue, newValue) {
if (name === "title" && oldValue !== newValue) this.updateTitle();
}
updateTitle() {
const title = this.getAttribute("title") || "";
const titleEl = this.shadowRoot?.querySelector(".title");
if (titleEl) titleEl.textContent = title;
}
}
class UiNavigation extends HTMLElement {
constructor() { super(); }
}
class UiNavMenu extends HTMLElement {
constructor() { super(); }
}
class UiNavItem extends HTMLElement {
static observedAttributes = ["active"];
constructor() { super(); }
connectedCallback() {
this.updateActive();
// Add click handler to update aria-current
const link = this.shadowRoot?.querySelector('.nav-item');
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
// Remove aria-current from all nav items
document.querySelectorAll('ui-nav-item').forEach(item => {
item.removeAttribute('active');
const itemLink = item.shadowRoot?.querySelector('.nav-item');
if (itemLink) {
itemLink.classList.remove('active');
itemLink.removeAttribute('aria-current');
}
});
// Add to clicked item
this.setAttribute('active', '');
link.classList.add('active');
link.setAttribute('aria-current', 'page');
});
}
}
attributeChangedCallback(name) {
if (name === "active") this.updateActive();
}
updateActive() {
const item = this.shadowRoot?.querySelector(".nav-item");
if (item) {
const isActive = this.hasAttribute("active");
item.classList.toggle("active", isActive);
if (isActive) {
item.setAttribute('aria-current', 'page');
} else {
item.removeAttribute('aria-current');
}
}
}
}
class UiContent extends HTMLElement {
constructor() { super(); }
}
class UiSection extends HTMLElement {
constructor() { super(); }
}
class UiRail extends HTMLElement {
constructor() { super(); }
}
class UiFooter extends HTMLElement {
static observedAttributes = ["status"];
constructor() { super(); }
connectedCallback() { this.updateStatus(); }
attributeChangedCallback(name, oldValue, newValue) {
if (name === "status" && oldValue !== newValue) this.updateStatus();
}
updateStatus() {
const status = this.getAttribute("status") || "";
const footerEl = this.shadowRoot?.querySelector(".footer");
if (footerEl) footerEl.textContent = status;
}
}
// Utility components - pure declarative
class UiDivider extends HTMLElement {
constructor() { super(); }
}
class UiButtonStack extends HTMLElement {
constructor() { super(); }
}
class UiInputGroup extends HTMLElement {
constructor() { super(); }
}
class UiThemeSwitcher extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
// Attach event listeners to buttons in shadow DOM
const buttons = this.shadowRoot.querySelectorAll('button');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const theme = btn.getAttribute('data-theme');
setTheme(theme);
});
});
// Update initial active state
const currentTheme = document.documentElement.getAttribute('data-theme') || 'auto';
buttons.forEach(btn => {
const theme = btn.getAttribute('data-theme');
btn.classList.toggle('active', theme === currentTheme);
});
}
}
class UiGridTable extends HTMLElement {
static observedAttributes = ['columns'];
constructor() {
super();
}
connectedCallback() {
// Set grid columns from attribute
const columns = this.getAttribute('columns') || '1fr';
const gridTable = this.shadowRoot?.querySelector('.grid-table');
if (gridTable) {
gridTable.style.setProperty('--columns', columns);
}
this.setupSorting();
this.setupRowSelection();
this.setupSelectAll();
this.setupKeyboardNavigation();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'columns' && oldValue !== newValue) {
const gridTable = this.shadowRoot?.querySelector('.grid-table');
if (gridTable) {
gridTable.style.setProperty('--columns', newValue);
}
}
}
setupSorting() {
const sortButtons = this.querySelectorAll('.sort-button');
sortButtons.forEach((button, columnIndex) => {
button.addEventListener('click', () => {
const currentSort = button.getAttribute('aria-sort');
// Reset all other columns
sortButtons.forEach(btn => {
if (btn !== button) {
btn.setAttribute('aria-sort', 'none');
const icon = btn.querySelector('span[aria-hidden]');
if (icon) {
icon.textContent = '↕';
icon.style.opacity = '0.3';
}
}
});
// Toggle current column
const newSort = currentSort === 'ascending' ? 'descending' : 'ascending';
button.setAttribute('aria-sort', newSort);
const icon = button.querySelector('span[aria-hidden]');
if (icon) {
icon.textContent = newSort === 'ascending' ? '↑' : '↓';
icon.style.opacity = '1';
}
// Sort the grid
this.sortGrid(columnIndex, newSort === 'ascending');
});
});
}
sortGrid(columnIndex, ascending) {
const allCells = Array.from(this.querySelectorAll('.cell'));
const numColumns = this.getAttribute('columns').split(' ').length;
// Group cells into rows
const rows = [];
for (let i = 0; i < allCells.length; i += numColumns) {
rows.push(allCells.slice(i, i + numColumns));
}
// Sort rows based on column
rows.sort((a, b) => {
const aText = a[columnIndex]?.textContent.trim() || '';
const bText = b[columnIndex]?.textContent.trim() || '';
return ascending ?
aText.localeCompare(bText) :
bText.localeCompare(aText);
});
// Re-append sorted cells
const headerCells = this.querySelectorAll('.header-cell');
rows.forEach(row => {
row.forEach(cell => this.appendChild(cell));
});
}
setupRowSelection() {
const checkboxes = this.querySelectorAll('.cell input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => {
this.updateSelectAllState();
});
});
}
setupSelectAll() {
const selectAll = this.querySelector('#grid-select-all');
if (selectAll) {
selectAll.addEventListener('change', () => {
const checkboxes = this.querySelectorAll('.cell input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked;
});
});
}
}
updateSelectAllState() {
const selectAll = this.querySelector('#grid-select-all');
const checkboxes = Array.from(this.querySelectorAll('.cell input[type="checkbox"]'));
const checkedCount = checkboxes.filter(cb => cb.checked).length;
if (selectAll) {
selectAll.checked = checkedCount === checkboxes.length;
selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
}
}
setupKeyboardNavigation() {
// Grid keyboard navigation would go here
}
}
class UiDateRangePicker extends HTMLElement {
constructor() {
super();
this.startDate = null;
this.endDate = null;
this.currentMonth = null;
this.currentYear = null;
}
connectedCallback() {
// Check for Temporal API support
if (typeof Temporal === 'undefined') {
console.warn('Temporal API not available. Using Date fallback.');
this.useFallback = true;
}
this.initializeCalendar();
this.render();
this.setupEventListeners();
}
initializeCalendar() {
if (this.useFallback) {
const now = new Date();
this.currentMonth = now.getMonth();
this.currentYear = now.getFullYear();
} else {
const now = Temporal.Now.plainDateISO();
this.currentMonth = now.month;
this.currentYear = now.year;
}
}
render() {
const monthYear = this.shadowRoot.querySelector('.month-year');
const grid = this.shadowRoot.querySelector('.calendar-grid');
const selectedRange = this.shadowRoot.querySelector('.selected-range');
if (!grid) return;
// Update month/year display
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
if (this.useFallback) {
monthYear.textContent = `${monthNames[this.currentMonth]} ${this.currentYear}`;
} else {
monthYear.textContent = `${monthNames[this.currentMonth - 1]} ${this.currentYear}`;
}
// Clear existing days (keep day labels)
const dayLabels = grid.querySelectorAll('.day-label');
grid.innerHTML = '';
dayLabels.forEach(label => grid.appendChild(label));
// Calculate calendar
const firstDay = this.useFallback ?
new Date(this.currentYear, this.currentMonth, 1).getDay() :
Temporal.PlainDate.from({ year: this.currentYear, month: this.currentMonth, day: 1 }).dayOfWeek % 7;
const daysInMonth = this.useFallback ?
new Date(this.currentYear, this.currentMonth + 1, 0).getDate() :
Temporal.PlainDate.from({ year: this.currentYear, month: this.currentMonth, day: 1 }).daysInMonth;
const today = this.useFallback ?
{ year: new Date().getFullYear(), month: new Date().getMonth(), day: new Date().getDate() } :
Temporal.Now.plainDateISO();
// Previous month days
const prevMonthDays = this.useFallback ?
new Date(this.currentYear, this.currentMonth, 0).getDate() :
Temporal.PlainDate.from({
year: this.currentMonth === 1 ? this.currentYear - 1 : this.currentYear,
month: this.currentMonth === 1 ? 12 : this.currentMonth - 1,
day: 1
}).daysInMonth;
for (let i = firstDay - 1; i >= 0; i--) {
const day = prevMonthDays - i;
this.createDayButton(day, true, grid);
}
// Current month days
for (let day = 1; day <= daysInMonth; day++) {
const isToday = this.useFallback ?
(this.currentYear === today.year && this.currentMonth === today.month && day === today.day) :
(this.currentYear === today.year && this.currentMonth === today.month && day === today.day);
this.createDayButton(day, false, grid, isToday);
}
// Next month days
const totalCells = firstDay + daysInMonth;
const remainingCells = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
for (let day = 1; day <= remainingCells; day++) {
this.createDayButton(day, true, grid);
}
// Update selected range display
this.updateRangeDisplay();
}
createDayButton(day, isOtherMonth, grid, isToday = false) {
const button = document.createElement('button');
button.className = `day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''}`;
button.textContent = day;
button.type = 'button';
button.setAttribute('role', 'gridcell');
if (!isOtherMonth) {
const dateStr = this.useFallback ?
`${this.currentYear}-${String(this.currentMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` :
`${this.currentYear}-${String(this.currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
button.dataset.date = dateStr;
button.setAttribute('aria-label', this.formatDateForAria(this.currentYear, this.currentMonth, day));
// Check if in range
if (this.startDate && this.endDate) {
const currentDate = this.useFallback ? new Date(dateStr) : Temporal.PlainDate.from(dateStr);
const start = this.useFallback ? new Date(this.startDate) : Temporal.PlainDate.from(this.startDate);
const end = this.useFallback ? new Date(this.endDate) : Temporal.PlainDate.from(this.endDate);
if (this.useFallback) {
if (currentDate >= start && currentDate <= end) {
button.classList.add('in-range');
}
if (currentDate.getTime() === start.getTime()) {
button.classList.add('selected-start');
}
if (currentDate.getTime() === end.getTime()) {
button.classList.add('selected-end');
}
} else {
const comp1 = Temporal.PlainDate.compare(currentDate, start);
const comp2 = Temporal.PlainDate.compare(currentDate, end);
if (comp1 >= 0 && comp2 <= 0) {
button.classList.add('in-range');
}
if (comp1 === 0) {
button.classList.add('selected-start');
}
if (comp2 === 0) {
button.classList.add('selected-end');
}
}
} else if (this.startDate) {
const currentDate = this.useFallback ? new Date(dateStr) : Temporal.PlainDate.from(dateStr);
const start = this.useFallback ? new Date(this.startDate) : Temporal.PlainDate.from(this.startDate);
if (this.useFallback) {
if (currentDate.getTime() === start.getTime()) {
button.classList.add('selected-start');
}
} else {
if (Temporal.PlainDate.compare(currentDate, start) === 0) {
button.classList.add('selected-start');
}
}
}
button.addEventListener('click', () => this.selectDate(dateStr));
} else {
button.disabled = true;
button.classList.add('disabled');
}
grid.appendChild(button);
}
formatDateForAria(year, month, day) {
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
const monthIndex = this.useFallback ? month : month - 1;
return `${monthNames[monthIndex]} ${day}, ${year}`;
}
selectDate(dateStr) {
if (!this.startDate || (this.startDate && this.endDate)) {
// Start new selection
this.startDate = dateStr;
this.endDate = null;
} else {
// Complete the range
const start = this.useFallback ? new Date(this.startDate) : Temporal.PlainDate.from(this.startDate);
const end = this.useFallback ? new Date(dateStr) : Temporal.PlainDate.from(dateStr);
// Ensure start is before end
if (this.useFallback) {
if (end < start) {
this.endDate = this.startDate;
this.startDate = dateStr;
} else {
this.endDate = dateStr;
}
} else {
if (Temporal.PlainDate.compare(end, start) < 0) {
this.endDate = this.startDate;
this.startDate = dateStr;
} else {
this.endDate = dateStr;
}
}
}
this.render();
}
updateRangeDisplay() {
const rangeDisplay = this.shadowRoot.querySelector('.selected-range');
const clearButton = this.shadowRoot.querySelector('.clear-button');
if (!rangeDisplay) return;
if (this.startDate && this.endDate) {
const start = new Date(this.startDate).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric'
});
const end = new Date(this.endDate).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric'
});
rangeDisplay.innerHTML = `<strong>Selected:</strong> ${start} → ${end}`;
clearButton.style.display = 'block';
} else if (this.startDate) {
const start = new Date(this.startDate).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric'
});
rangeDisplay.innerHTML = `<strong>Start:</strong> ${start} <em>(select end date)</em>`;
clearButton.style.display = 'block';
} else {
rangeDisplay.textContent = 'Select a date range';
clearButton.style.display = 'none';
}
}
setupEventListeners() {
const prevButton = this.shadowRoot.querySelectorAll('.nav-button')[0];
const nextButton = this.shadowRoot.querySelectorAll('.nav-button')[1];
const clearButton = this.shadowRoot.querySelector('.clear-button');
prevButton?.addEventListener('click', () => {
if (this.useFallback) {
this.currentMonth--;
if (this.currentMonth < 0) {
this.currentMonth = 11;
this.currentYear--;
}
} else {
if (this.currentMonth === 1) {
this.currentMonth = 12;
this.currentYear--;
} else {
this.currentMonth--;
}
}
this.render();
});
nextButton?.addEventListener('click', () => {
if (this.useFallback) {
this.currentMonth++;
if (this.currentMonth > 11) {
this.currentMonth = 0;
this.currentYear++;
}
} else {
if (this.currentMonth === 12) {
this.currentMonth = 1;
this.currentYear++;
} else {
this.currentMonth++;
}
}
this.render();
});
clearButton?.addEventListener('click', () => {
this.startDate = null;
this.endDate = null;
this.render();
});
}
}
// Register all components
customElements.define("ui-layout", UiLayout);
customElements.define("ui-appbar", UiAppBar);
customElements.define("ui-navigation", UiNavigation);
customElements.define("ui-nav-menu", UiNavMenu);
customElements.define("ui-nav-item", UiNavItem);
customElements.define("ui-content", UiContent);
customElements.define("ui-section", UiSection);
customElements.define("ui-rail", UiRail);
customElements.define("ui-footer", UiFooter);
customElements.define("ui-divider", UiDivider);
customElements.define("ui-button-stack", UiButtonStack);
customElements.define("ui-input-group", UiInputGroup);
customElements.define("ui-theme-switcher", UiThemeSwitcher);
customElements.define("ui-grid-table", UiGridTable);
customElements.define("ui-date-range-picker", UiDateRangePicker);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment