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.
Citations table
On/off toggle on a native checkbox, in two sizes and two tones.
InputsAtomNo dependencies Beautiful UI, MIT
Marketing automations
Citations table
foundation.css holds the tokens every component reads.
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
--- ---
import Switch from "../components/viewership/switch/Switch.astro";
---
<Switch /> | Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | false | Initial state. |
disabled | boolean | false | Disable the switch. |
name / value / id | string | · | Passed to the native checkbox. |
label | string | · | Accessible name if no visible label. |
size | "md" | "sm" | "md" | md 40×24; sm 30×18 (24px hit area). |
tone | "ink" | "accent" | "ink" | Track color when on. |
labelPosition | "start" | "end" | "end" | Slotted label position. |
| Event | Detail | When |
|---|---|---|
change | Event | Native change event. Read event.target.checked. |
---
/**
* Switch: an on/off toggle on a native checkbox.
*
* Ported from Beautiful UI `components/atoms/Switch.tsx`
* (MIT, © 2026 Shane Levine), plus the compact accent switch used by the
* upstream RecordsTable (`size="sm" tone="accent"`).
*
* Built on `<input type="checkbox" role="switch">`: no script, Space toggles
* it, it submits with forms, and it fires the native `change` event. The
* default slot is the visible label; without one, pass `label`.
*
* @example
* <Switch checked name="autoDraft">Auto-draft weekly report</Switch>
*/
import type { HTMLAttributes } from "astro/types";
type Props = Omit<HTMLAttributes<"label">, "for"> & {
checked?: boolean;
disabled?: boolean;
name?: string;
value?: string;
/** id for the input (e.g. for aria-describedby targets elsewhere). */
id?: string;
/** Accessible name when there is no visible label in the slot. */
label?: string;
size?: "md" | "sm";
/** Track color when on. */
tone?: "ink" | "accent";
/** Where the slotted label sits. */
labelPosition?: "start" | "end";
};
const {
checked = false,
disabled = false,
name,
value,
id,
label,
size = "md",
tone = "ink",
labelPosition = "end",
class: className,
...rest
} = Astro.props;
const hasText = Astro.slots.has("default");
---
<label
class:list={["vs-switch", `vs-switch--${size}`, `vs-switch--${tone}`, { "vs-switch--disabled": disabled }, className]}
{...rest}
>
{hasText && labelPosition === "start" && <span class="vs-switch__label"><slot /></span>}
<span class="vs-switch__control">
<input
class="vs-switch__input"
type="checkbox"
role="switch"
id={id}
name={name}
value={value}
checked={checked}
disabled={disabled}
aria-label={hasText ? undefined : label}
/>
<span class="vs-switch__track" aria-hidden="true"><span class="vs-switch__thumb"></span></span>
</span>
{hasText && labelPosition === "end" && <span class="vs-switch__label"><slot /></span>}
</label>
<style>
.vs-switch {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: var(--vs-ink);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.vs-switch--disabled {
cursor: not-allowed;
opacity: 0.5;
}
.vs-switch__control {
position: relative;
display: inline-flex;
flex-shrink: 0;
}
/* The real checkbox sits invisibly over the track (and pads small sizes to a 24px target). */
.vs-switch__input {
position: absolute;
inset: -3px;
z-index: 1;
width: auto;
height: auto;
margin: 0;
opacity: 0;
cursor: inherit;
}
.vs-switch__track {
position: relative;
display: block;
width: var(--vs-switch-w);
height: var(--vs-switch-h);
border-radius: var(--vs-radius-pill);
background: var(--vs-line-strong);
transition: background-color var(--vs-switch-duration) ease-out;
}
.vs-switch__thumb {
position: absolute;
top: 2px;
left: 2px;
width: var(--vs-switch-thumb);
height: var(--vs-switch-thumb);
border-radius: var(--vs-radius-pill);
background: white;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transition:
transform var(--vs-switch-duration) var(--vs-ease-out),
background-color var(--vs-switch-duration) ease-out;
}
.vs-switch__input:checked + .vs-switch__track .vs-switch__thumb {
transform: translateX(calc(var(--vs-switch-w) - var(--vs-switch-thumb) - 4px));
}
.vs-switch__input:focus-visible + .vs-switch__track {
box-shadow: var(--vs-focus-ring);
}
/* Sizes */
.vs-switch--md {
--vs-switch-w: 40px;
--vs-switch-h: 24px;
--vs-switch-thumb: 20px;
--vs-switch-duration: 200ms;
}
.vs-switch--sm {
--vs-switch-w: 30px;
--vs-switch-h: 18px;
--vs-switch-thumb: 14px;
--vs-switch-duration: 150ms;
gap: 8px;
font-size: 13px;
}
.vs-switch--sm .vs-switch__thumb {
box-shadow: var(--vs-shadow-btn);
}
/* Tones */
.vs-switch--ink .vs-switch__input:checked + .vs-switch__track {
background: var(--vs-ink);
}
/* Keeps the thumb readable on the light ink track in dark mode. */
.vs-switch--ink .vs-switch__input:checked + .vs-switch__track .vs-switch__thumb {
background: var(--vs-ink-inverse);
}
.vs-switch--accent .vs-switch__input:checked + .vs-switch__track {
background: var(--vs-accent);
}
@media (prefers-reduced-motion: reduce) {
.vs-switch__track,
.vs-switch__thumb {
transition: none;
}
}
@media (forced-colors: active) {
.vs-switch__track,
.vs-switch__thumb {
forced-color-adjust: none;
}
.vs-switch .vs-switch__track {
background: Canvas;
outline: 1px solid CanvasText;
}
.vs-switch .vs-switch__thumb,
.vs-switch .vs-switch__input:checked + .vs-switch__track .vs-switch__thumb {
background: CanvasText;
}
.vs-switch .vs-switch__input:checked + .vs-switch__track {
background: Highlight;
}
}
</style> Ported from Beautiful UI, MIT. Notices.
Used by Records Table