Astro UI
The foundation.
One stylesheet. Every color, radius, shadow and easing the components read. 55 tokens.
-
Import it once
Save it as
src/styles/foundation.css. Paper or ink follows the OS.--- // src/layouts/Layout.astro import "../styles/foundation.css"; --- -
Scope a mode or theme
Any element can force paper or ink, or the neutral theme.
<!-- force a mode or theme on any element --> <section data-vs-mode="dark">…</section> <section data-vs-theme="neutral" data-vs-mode="light">…</section>
On paper.
Surfaces
-
page#f2ece4 -
canvas#e8e0d5 -
surface#fbf8f3 -
inset#f6f1ea -
field#ede6dc -
hover#f0e9e0 -
hover-2#e5dccf
Ink
-
ink#0d0809 -
ink-2#5a4f50 -
ink-3#7a6f6e -
ink-inverse#f2ece4
Lines
-
line#e3dbd0 -
line-strong#d2c7ba -
line-soft#ece5db -
elev-ring#e3dbd0 -
elev-ring-strong#d2c7ba -
grid-linemix
Accent
-
accent#e1232a -
accent-ink#b81a20 -
accent-tint#f8dde5 -
on-accent#ffffff
Semantic
-
green#357045 -
green-tint#dfe8da -
orange#9e5812 -
orange-tint#f3e2c8 -
red#b0341b -
red-tint#f4d9cc
Tooltip
-
tooltip-bg#0d0809 -
tooltip-fg#f2ece4 -
tooltip-muted#a99e97 -
tooltip-border#2b2023
On ink.
Surfaces
-
page#0d0809 -
canvas#120b0d -
surface#1a1214 -
inset#150e10 -
field#231a1c -
hover#221a1c -
hover-2#2c2225
Ink
-
ink#f2ece4 -
ink-2#b5aba3 -
ink-3#8a7f7a -
ink-inverse#0d0809
Lines
-
line#2a2023 -
line-strong#3a2e31 -
line-soft#211819 -
elev-ringrgb(242 236 228 / 0.09) -
elev-ring-strongrgb(242 236 228 / 0.13) -
grid-linemix
Accent
-
accent#e8272a -
accent-ink#f7a9b8 -
accent-tintrgb(232 39 42 / 0.18) -
on-accent#ffffff
Semantic
-
green#7fbf8e -
green-tintrgb(127 191 142 / 0.14) -
orange#e2a35c -
orange-tintrgb(226 163 92 / 0.14) -
red#f08a6b -
red-tintrgb(240 138 107 / 0.14)
Tooltip
-
tooltip-bg#2a1f22 -
tooltip-fg#f2ece4 -
tooltip-muted#a99e97 -
tooltip-border#3a2e31
Type
- Weekly report is ready 16 / 600
- Approve 3 campaign briefs 14 / 500
- Sessions up 12% week over week, led by organic search. 13 / 400
- Updated 4 min ago · 1,284 rows 12 / 400
-
const report = await weekly({ site });Code only
Archivo at normal width. Numbers use tabular figures.
Radii
- chip 4px
- control 6px
- card 8px
- window 12px
- pill
Elevation
- hairline
- btn
- card
- raised
- overlay
Motion
- ease-out
- ease-in-out
- ease-link
Hover to play. Reduced motion skips every transition.
Two themes.
- #214Spring Search Live
- #215Trail Test video In review
- #216Gear Guide email Needs approval
- #217Reddit AMA post Rejected
- #218Summer promo Queued
Marketing automations
Auto-send weekly reportEmail the GA4 and Ads summary every Monday.
Alert on new AI citationsPost in Slack when ChatGPT cites a page.
Brand voice checkManaged by the content lead.
AI citations
412cites 27 new across 9 topics
Engine
Viewership ships as the default. Neutral is the original Beautiful UI look, in the same file.
Source
/* ──────────────────────────────────────────────────────────
* Viewership UI: foundation
*
* The one stylesheet every component needs. Import it once, globally:
*
* import "../styles/foundation.css";
*
* Contents: design tokens (Viewership paper + ink, and a neutral
* alternate), shared keyframes, and the reduced-motion baseline.
* Components never hard-code colors, radii, shadows or easings; they read
* these tokens, so a theme is just a different set of values.
*
* Token system adapted from Beautiful UI (MIT, © 2026 Shane Levine,
* https://www.beautifului.dev); see THIRD_PARTY_NOTICES.md.
*
* Mode (paper or ink), on any element:
* - default follows the OS (prefers-color-scheme)
* - <html data-theme="light|dark"> forces one for the page
* - data-vs-mode="light|dark" forces one for an element and its subtree
* Theme:
* - Viewership (paper + ink, signal accent) is the default
* - data-vs-theme="neutral" restores the original Beautiful UI look; put it
* on the same element as data-vs-mode
* ────────────────────────────────────────────────────────── */
/* ── Viewership, paper (light) ── */
:root,
[data-vs-mode="light"] {
color-scheme: light;
/* Surfaces, back to front */
--vs-page: #f2ece4;
--vs-canvas: #e8e0d5;
--vs-surface: #fbf8f3;
--vs-inset: #f6f1ea;
--vs-field: #ede6dc;
--vs-hover: #f0e9e0;
--vs-hover-2: #e5dccf;
/* Ink ramp: primary, secondary, tertiary */
--vs-ink: #0d0809;
--vs-ink-2: #5a4f50;
--vs-ink-3: #7a6f6e;
/* Ink on top of a filled --vs-ink surface (primary buttons) */
--vs-ink-inverse: #f2ece4;
/* Hairlines */
--vs-line: #e3dbd0;
--vs-line-strong: #d2c7ba;
--vs-line-soft: #ece5db;
/* Accent + semantic condiments (use sparingly) */
--vs-accent: #e1232a;
--vs-accent-ink: #b81a20;
--vs-accent-tint: #f8dde5;
--vs-on-accent: #ffffff;
--vs-green: #357045;
--vs-green-tint: #dfe8da;
--vs-orange: #9e5812;
--vs-orange-tint: #f3e2c8;
--vs-red: #b0341b;
--vs-red-tint: #f4d9cc;
/* Tooltips / dark popovers (stay dark in both modes) */
--vs-tooltip-bg: #0d0809;
--vs-tooltip-fg: #f2ece4;
--vs-tooltip-muted: #a99e97;
--vs-tooltip-border: #2b2023;
/* Elevation: warm, ink-tinted, low. Rings are the hairline of a raised surface. */
--vs-elev-ring: #e3dbd0;
--vs-elev-ring-strong: #d2c7ba;
--vs-shadow-xs: 0 1px 1px rgb(13 8 9 / 0.04), 0 1px 2px rgb(13 8 9 / 0.04);
--vs-shadow-sm: 0 1px 2px rgb(13 8 9 / 0.04), 0 2px 4px rgb(13 8 9 / 0.04);
--vs-shadow-md: 0 2px 4px rgb(13 8 9 / 0.04), 0 6px 14px rgb(13 8 9 / 0.05);
--vs-shadow-lg: 0 4px 8px rgb(13 8 9 / 0.05), 0 16px 36px rgb(13 8 9 / 0.09);
--vs-shadow-inset-field: inset 0 1px 2px rgb(13 8 9 / 0.1);
/* Type: Archivo at its normal width for UI; mono is for code only */
--vs-font-sans: "Archivo Variable", "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--vs-font-stretch: 100%;
--vs-font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
/* Radii */
--vs-radius-chip: 4px;
--vs-radius-control: 6px;
--vs-radius-card: 8px;
--vs-radius-window: 12px;
--vs-radius-pill: 999px;
/* Motion */
--vs-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--vs-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--vs-ease-link: cubic-bezier(0.16, 1, 0.3, 1);
--vs-duration-fast: 150ms;
--vs-duration: 300ms;
}
/* ── Viewership, ink (dark) ── */
[data-vs-mode="dark"],
:root[data-theme="dark"] {
color-scheme: dark;
--vs-page: #0d0809;
--vs-canvas: #120b0d;
--vs-surface: #1a1214;
--vs-inset: #150e10;
--vs-field: #231a1c;
--vs-hover: #221a1c;
--vs-hover-2: #2c2225;
--vs-ink: #f2ece4;
--vs-ink-2: #b5aba3;
--vs-ink-3: #8a7f7a;
--vs-ink-inverse: #0d0809;
--vs-line: #2a2023;
--vs-line-strong: #3a2e31;
--vs-line-soft: #211819;
--vs-accent: #e8272a;
--vs-accent-ink: #f7a9b8;
--vs-accent-tint: rgb(232 39 42 / 0.18);
--vs-green: #7fbf8e;
--vs-green-tint: rgb(127 191 142 / 0.14);
--vs-orange: #e2a35c;
--vs-orange-tint: rgb(226 163 92 / 0.14);
--vs-red: #f08a6b;
--vs-red-tint: rgb(240 138 107 / 0.14);
--vs-tooltip-bg: #2a1f22;
--vs-tooltip-border: #3a2e31;
--vs-elev-ring: rgb(242 236 228 / 0.09);
--vs-elev-ring-strong: rgb(242 236 228 / 0.13);
--vs-shadow-xs: 0 1px 2px rgb(0 0 0 / 0.35);
--vs-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.25), 0 2px 6px rgb(0 0 0 / 0.25);
--vs-shadow-md: 0 2px 10px rgb(0 0 0 / 0.3);
--vs-shadow-lg: 0 8px 28px rgb(0 0 0 / 0.45);
--vs-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 0.45);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--vs-page: #0d0809;
--vs-canvas: #120b0d;
--vs-surface: #1a1214;
--vs-inset: #150e10;
--vs-field: #231a1c;
--vs-hover: #221a1c;
--vs-hover-2: #2c2225;
--vs-ink: #f2ece4;
--vs-ink-2: #b5aba3;
--vs-ink-3: #8a7f7a;
--vs-ink-inverse: #0d0809;
--vs-line: #2a2023;
--vs-line-strong: #3a2e31;
--vs-line-soft: #211819;
--vs-accent: #e8272a;
--vs-accent-ink: #f7a9b8;
--vs-accent-tint: rgb(232 39 42 / 0.18);
--vs-green: #7fbf8e;
--vs-green-tint: rgb(127 191 142 / 0.14);
--vs-orange: #e2a35c;
--vs-orange-tint: rgb(226 163 92 / 0.14);
--vs-red: #f08a6b;
--vs-red-tint: rgb(240 138 107 / 0.14);
--vs-tooltip-bg: #2a1f22;
--vs-tooltip-border: #3a2e31;
--vs-elev-ring: rgb(242 236 228 / 0.09);
--vs-elev-ring-strong: rgb(242 236 228 / 0.13);
--vs-shadow-xs: 0 1px 2px rgb(0 0 0 / 0.35);
--vs-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.25), 0 2px 6px rgb(0 0 0 / 0.25);
--vs-shadow-md: 0 2px 10px rgb(0 0 0 / 0.3);
--vs-shadow-lg: 0 8px 28px rgb(0 0 0 / 0.45);
--vs-shadow-inset-field: inset 0 1px 2px rgb(0 0 0 / 0.45);
}
}
/* ── Neutral (the original Beautiful UI look), light ── */
[data-vs-theme="neutral"] {
color-scheme: light;
--vs-page: oklch(0.985 0.001 286.376);
--vs-canvas: oklch(0.961 0.002 247.84);
--vs-surface: oklch(1 0 0);
--vs-inset: oklch(0.979 0.002 247.839);
--vs-field: oklch(0.961 0.001 286.375);
--vs-hover: oklch(0.97 0.002 247.839);
--vs-hover-2: oklch(0.933 0.003 247.86);
--vs-ink: oklch(0.247 0.006 258.361);
--vs-ink-2: oklch(0.506 0.01 264.477);
--vs-ink-3: oklch(0.695 0.009 264.505);
--vs-ink-inverse: oklch(0.985 0.001 286.376);
--vs-line: oklch(0.946 0.003 264.542);
--vs-line-strong: oklch(0.912 0.005 258.326);
--vs-line-soft: oklch(0.966 0.002 264.542);
--vs-accent: oklch(0.626 0.205 254.947);
--vs-accent-ink: oklch(0.556 0.187 255.617);
--vs-accent-tint: oklch(0.96 0.019 252.878);
--vs-on-accent: #ffffff;
--vs-green: oklch(0.603 0.155 150.883);
--vs-green-tint: oklch(0.958 0.017 159.118);
--vs-orange: oklch(0.689 0.179 49.902);
--vs-orange-tint: oklch(0.964 0.021 67.581);
--vs-red: oklch(0.621 0.192 23.042);
--vs-red-tint: oklch(0.956 0.017 17.462);
--vs-tooltip-bg: oklch(0.272 0.008 264.435);
--vs-tooltip-fg: oklch(0.976 0.002 247.839);
--vs-tooltip-muted: oklch(0.731 0.008 260.731);
--vs-tooltip-border: oklch(0.356 0.007 264.474);
--vs-elev-ring: oklch(0.946 0.003 264.542);
--vs-elev-ring-strong: oklch(0.912 0.005 258.326);
--vs-shadow-xs: 0 1px 1px oklch(0 0 0 / 0.04), 0 1px 2px oklch(0 0 0 / 0.04);
--vs-shadow-sm: 0 1px 2px oklch(0 0 0 / 0.04), 0 2px 4px oklch(0 0 0 / 0.04);
--vs-shadow-md: 0 2px 4px oklch(0 0 0 / 0.04), 0 6px 14px oklch(0 0 0 / 0.06);
--vs-shadow-lg: 0 4px 8px oklch(0 0 0 / 0.05), 0 16px 36px oklch(0 0 0 / 0.1);
--vs-shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.12);
--vs-font-sans: "Inter Variable", "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--vs-font-stretch: 100%;
--vs-font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
--vs-radius-chip: 6px;
--vs-radius-control: 8px;
--vs-radius-card: 10px;
--vs-radius-window: 14px;
--vs-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}
/* ── Neutral, dark ── */
[data-vs-theme="neutral"][data-vs-mode="dark"],
:root[data-theme="dark"][data-vs-theme="neutral"] {
color-scheme: dark;
--vs-page: oklch(0.209 0.004 264.477);
--vs-canvas: oklch(0.231 0.004 264.487);
--vs-surface: oklch(0.26 0.006 271.191);
--vs-inset: oklch(0.243 0.004 264.492);
--vs-field: oklch(0.293 0.006 271.223);
--vs-hover: oklch(0.289 0.006 271.22);
--vs-hover-2: oklch(0.318 0.007 274.747);
--vs-ink: oklch(0.964 0.002 247.839);
--vs-ink-2: oklch(0.731 0.008 260.731);
--vs-ink-3: oklch(0.541 0.01 264.484);
--vs-ink-inverse: oklch(0.231 0.004 264.487);
--vs-line: oklch(0.308 0.006 258.354);
--vs-line-strong: oklch(0.356 0.007 264.474);
--vs-line-soft: oklch(0.278 0.006 258.354);
--vs-accent: oklch(0.68 0.173 253.301);
--vs-accent-ink: oklch(0.788 0.113 248.33);
--vs-accent-tint: oklch(0.68 0.173 253.301 / 0.16);
--vs-green: oklch(0.705 0.154 153.814);
--vs-green-tint: oklch(0.705 0.154 153.814 / 0.14);
--vs-orange: oklch(0.746 0.156 55.642);
--vs-orange-tint: oklch(0.746 0.156 55.642 / 0.14);
--vs-red: oklch(0.666 0.18 21.433);
--vs-red-tint: oklch(0.666 0.18 21.433 / 0.14);
--vs-tooltip-bg: oklch(0.182 0.004 264.459);
--vs-tooltip-border: oklch(0.308 0.006 258.354);
--vs-elev-ring: oklch(1 0 0 / 0.11);
--vs-elev-ring-strong: oklch(1 0 0 / 0.15);
--vs-shadow-xs: 0 1px 2px oklch(0 0 0 / 0.3);
--vs-shadow-sm: 0 1px 2px oklch(0 0 0 / 0.2), 0 2px 6px oklch(0 0 0 / 0.2);
--vs-shadow-md: 0 2px 10px oklch(0 0 0 / 0.22);
--vs-shadow-lg: 0 8px 28px oklch(0 0 0 / 0.34);
--vs-shadow-inset-field: inset 0 1px 2px oklch(0 0 0 / 0.4);
}
/* ── Derived tokens ──
* Re-declared on every element that sets a mode or theme, so their var()
* references resolve against that element's palette, not the page's. */
:root,
[data-vs-mode],
[data-vs-theme] {
--vs-grid-line: color-mix(in srgb, var(--vs-line) 78%, transparent);
--vs-shadow-hairline: 0 0 0 1px var(--vs-line);
--vs-shadow-btn: 0 0 0 1px var(--vs-elev-ring-strong), var(--vs-shadow-xs);
--vs-shadow-card: 0 0 0 1px var(--vs-elev-ring), var(--vs-shadow-sm);
--vs-shadow-raised: 0 0 0 1px var(--vs-elev-ring), var(--vs-shadow-md);
--vs-shadow-overlay: 0 0 0 1px var(--vs-elev-ring-strong), var(--vs-shadow-lg);
--vs-focus-ring: 0 0 0 2px var(--vs-surface), 0 0 0 3.5px var(--vs-accent);
}
/* A scoped mode or theme also sets the text basics, so components never
* inherit the host page's colour or type. */
[data-vs-mode],
[data-vs-theme] {
color: var(--vs-ink);
font-family: var(--vs-font-sans);
font-stretch: var(--vs-font-stretch);
}
/* ── Shared keyframes (prefixed so they never collide with app CSS) ── */
@keyframes vs-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes vs-fade-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes vs-pop-in {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes vs-shimmer-text {
from { background-position: 150% center; }
to { background-position: -50% center; }
}
@keyframes vs-pixel-on {
0%, 100% { opacity: 0.15; }
18%, 42% { opacity: 1; }
62% { opacity: 0.15; }
}
@keyframes vs-spin {
to { transform: rotate(360deg); }
}
@keyframes vs-caret-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes vs-pulse {
0%, 100% { opacity: 0.35; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1); }
}
/* ── Shared utilities ── */
/* `hidden` always wins, even over a component's own `display` rule. */
:where([hidden]) {
display: none !important;
}
/* Visually hidden but announced by screen readers. */
.vs-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ── Reduced motion: components still render their end state ── */
@media (prefers-reduced-motion: reduce) {
:root {
--vs-duration-fast: 0ms;
--vs-duration: 0ms;
}
} Third-party notices
# Third-party notices Viewership UI ports components and design tokens from the projects below. Their licenses require this notice to accompany copies of the software. ## Beautiful UI https://www.beautifului.dev · https://github.com/slev12397/beautiful-ui MIT License Copyright (c) 2026 Shane Levine Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## shadcn/ui https://ui.shadcn.com · https://github.com/shadcn-ui/ui MIT License Copyright (c) 2023 shadcn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.