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
Using Claude Code? Give it the agent prompt and skip these steps.
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