Imported from home-assistant/home-assistant.io (
.claude/skills/create-astro-component/SKILL.md). Install upstream withnpx skills add home-assistant/home-assistant.io --skill create-astro-component. Copyright stays with the author.
Create an Astro component from the Jekyll site
Recreate a piece of the current website as a native Astro component in
astro/src/components/. The goal is a component that looks the same
as the Jekyll original but whose code is as clean and tidy as if the
site had been built in Astro from day one.
Ask how much to bring over, before writing anything
The Astro migration is a chance to fix things, not just to move them. Never assume a 1:1 copy is wanted. For every component, ask the user first — in one short question with options — how faithful the port should be:
- Copy — match the live rendering as closely as possible, including its quirks.
- Copy the structure, clean the styling — keep the markup, layout and behavior, but replace fussy per-context styling with one simple, consistent system. Usually the right answer for chrome.
- Revamp — keep the structure and layout, deliberately redesign the surface (spacing, states, typography) to something cleaner.
Ask in the same breath which parts they want to keep and which "weirdness" they want dropped. The live design is evidence of intent, not a specification. When the user picks 2 or 3, prefer fewer rules over faithful ones: one link style for a whole navigation beats four context-specific ones, even if the original had four. Fidelity is about structure, layout and behavior — not about reproducing the original's exact sizes and radii.
Record the answer in the component's header comment, so a later reader knows which differences from the live site are deliberate.
Ground truth is the built site, never the source alone
The Jekyll source is misleading on its own: markup is generated by 30
Ruby Liquid plugins (plugins/) and 160 includes
(source/_includes/), and the CSS is scattered across sass/
partials (including the float-era inuitcss framework) with overrides
layered on overrides. What the visitor actually gets is the only
reliable reference. Work from:
- Rendered HTML: fetch real pages containing the component from
the live site (
https://www.home-assistant.io/...) and extract the component's markup. Look at more than one instance — the component may have states or contexts a single page does not show. - Compiled CSS: fetch the live site's compiled stylesheet (linked
from any page's
<head>) and collect every rule whose selector can hit the component's elements or classes, including media queries, hover/focus states, and inherited values that matter (font, color, line-height often come from ancestors). Missing an override is the main way ports come out subtly wrong. - The
sass/andplugins//_includes/sources are still useful as a cross-check for intent (and for finding all usage sites), just never as the sole source of truth. - Screenshot the original at desktop and mobile widths before writing
code, so there is a reference to compare against, and measure the
boxes and computed styles of its key elements. Use the scripts in
scripts/(see "Screenshots and measurements" below).
Two kinds of components — different fidelity contracts
Decide which kind this is before writing markup, and say so:
- Content components (ports of Liquid tags or includes that render inside documentation content — alerts, configuration blocks, glossary terms). Their rendered HTML gets diffed against Jekyll's output by the migration's parity checks, so keep the DOM structure and class names of the Jekyll output. Cleanups live in the CSS and the component's internals; deviations from the rendered markup need to be flagged and agreed.
- Chrome components (header, footer, sidebars, standalone page furniture). Parity is judged visually, not byte-by-byte, so the markup itself may be improved: semantic elements, fewer wrapper divs, flex/grid instead of float hacks — as long as the rendered result looks the same.
Code standards
These are the standards for all new Astro component code. The point is one consistent system instead of the accumulated mix in the old CSS.
- Units:
remfor every size (root is 16px, so divide px values by 16). The single exception: 1px borders and hairlines may staypx. Noemchains, no unitless magic numbers. - Do not port specific values — snap to the theme. Font sizes,
border radii, spacing, shadows and one-off edge cases in the old
CSS are accumulated accidents, not a design system. Take the
intent (small badge, rounded panel, pill button) and express it
with the nearest step in
astro/src/styles/tokens.scss, even when that shifts the result a pixel or two. A0.625rembadge becomesvar(--font-size-small); a0.25remradius becomesvar(--radius-small);min-width: 9.375remon a dropdown link just goes away. If the nearest step is clearly wrong for the component, that is a sign the scale needs a step — add it to the tokens, do not hard-code past it. The overall theme shapes the component, not the other way around. - Weird values are questions, not facts.
15.96px,0.9375em,13pxfont sizes and similar are usually rounding artifacts. Where the value is structural rather than thematic (an asset's aspect ratio, a breakpoint), propose the nearest sensible value and ask the user before replicating it. Better still, remove the need for it: a logo<img>withheightandwidth: autoneeds no magic width at all. - Breakpoints come from
astro/src/styles/_breakpoints.scss(@use "../styles/breakpoints";), which holds the three widths the site has always used. Media queries cannot read custom properties, so these are SCSS variables. Do not invent new breakpoints. - Styles are SCSS: every component's style block is
<style lang="scss">(thesasspackage is a dev dependency ofastro/). Use nesting and&to keep related rules together and to avoid repeating long selector chains; shared files inastro/src/styles/are.scsstoo. Keep the nesting shallow — it is there to shorten the CSS, not to rebuild the old override pyramid. - Tokens are the theme, in
astro/src/styles/tokens.scss: colors, type sizes, spacing, radii, shadows and layout metrics. Reuse an existing token when one fits; name tokens by role (--color-text-muted), not by hue. Variants such as a dark header should redefine a couple of local custom properties on the root element rather than duplicating selectors. - Scoped styles in the component's own
<style>block. No global leakage, no!important, no styling by tag name reaching outside the component. - Mobile-first: base styles for small screens,
min-widthmedia queries (in rem) for larger ones — even when the original CSS is desktop-first. - Modern layout: floats, clearfixes, and spacer elements in the original are implementation details of 2015, not the design. Recreate the visual result with flex or grid.
- Props and slots derive from how the Jekyll side is actually
used: enumerate real usage sites (grep
source/for the tag, include, or markup) and let the variations found there define the API. Do not invent options nothing uses. - Keep vendor prefixes out; the build targets modern browsers.
- Sub-pixel typography is noise. Letter spacing such as
0.005em(0.07px at 14px) and the third or fourth line-height ratio in a component cannot be seen. Drop them; a component needs one body line height and at most one for headings. - A negative margin that cancels another margin is an override. Give the preceding element the smaller margin it should have had instead. Negative margins are only for deliberate overlaps.
- Spacing that is off the scale goes. A
4pxlist-item padding or a16pxbottom margin that fights a24pxone above it is a leftover, not a design. Snap to a token or remove the rule and let line height do the work. - Fixed-size artwork that gets clipped may be deliberate. The footer skyline keeps its drawn size on phones and is cut off at the edge because the house in it is proportioned to the wordmark below: they form one lockup, and scaling the skyline breaks it. Before making clipped artwork responsive, check whether it is sized in relation to something else on the page, and ask when unsure.
- Images get
widthandheightattributes so the layout does not shift while they load. An SVG that only has aviewBoxrenders at 300×150 without them. - A block image inside an inline link stretches the link across the
line. Make the link a shrink-to-fit block (
display: block; width: fit-content) so the clickable area is the image, not the column. - No element resets inside the root selector. Astro's scoping adds
an attribute to every selector, so
.root h3, .root ul { margin: 0 }outranks a later plainul { margin-bottom: … }and silently wins. Set each element's full margin once, where it is styled.
When to stop and ask
Some things must go to the user as an explicit question with options,
never a silent decision either way (this mirrors the migration's
ground rules in astro/README.md):
- The original couples markup to JavaScript, Liquid logic, or global CSS in a way that has no clean Astro equivalent.
- A weird value (size, color, spacing) where matching and cleaning up conflict — present the original, the proposed standard value, and a recommendation.
- The original renders differently in different places and it is unclear which variant is canonical.
- Anything where "make it look the same" and "make it clean" genuinely cannot both be satisfied.
Batch these questions where possible instead of asking one at a time.
Deliverables
Every component ships as a set:
astro/src/components/<Name>.astro— the component, scoped styles, a short comment naming the Jekyll source it replaces (plugin, include, or sass partial paths).astro/src/components/<Name>.fixtures.mjs— pure-data fixtures (seeastro/README.md): one variant per meaningful state,propsandslotper variant, and the equivalent Jekyllliquidsource for content components so the variants can drive the golden-output parity tests.- A build check:
npx pnpm run buildinastro/must pass, and the component must render correctly in the component browser (/component-preview/) and its full-screen stage (/component-preview/<name>/). - Comparison images for the pull request: the Jekyll original next
to the Astro stage, one image per width, at desktop (1280px) and
mobile (400px), plus the lap width (800px) when the component's
layout changes there. Build them with
scripts/compare.mjsand save them as<name>-compare-<width>.pngin the scratchpad directory. Hand the paths to the user in the final message: they go into the pull request description (a human opens the pull request, per the AI policy), and they are the evidence on which match-or-improve is judged. Look at them yourself first and fix what does not match before presenting them.
Suggested workflow
- Ask how faithful the port should be (see above) and what to drop.
- Find the component in the wild: which pages render it, which plugin/include/sass files produce it, every usage variation.
- Capture ground truth: rendered HTML instances, the complete effective CSS rule set, reference screenshots at both widths.
- Decide content vs. chrome; state the fidelity contract.
- Collect the oddities (weird values, Astro conflicts) and ask about them in one batch.
- Build the component and fixtures to the standards above.
- Build, view in the component browser and stage, screenshot the stage at the same widths as the original, compose the comparison images and present them with their paths.
Screenshots and measurements
The scripts in scripts/ do the screenshot work. They need
Playwright, which is not part of the repository or the
development container. Install it once per session into the
scratchpad directory, never into the repository:
mkdir -p "$SCRATCHPAD/playwright" && cd "$SCRATCHPAD/playwright"
npm init -y >/dev/null && npm i playwright
npx playwright install chromium
# Chromium needs system libraries the container lacks:
sudo -n env PATH="$PATH" npx playwright install-deps chromium
export PLAYWRIGHT_DIR="$SCRATCHPAD/playwright"
cd - # back to the repository
Then, from the repository root, with
SK=.claude/skills/create-astro-component/scripts:
node $SK/screenshot.mjs <url> <out.png> <width> [selector] [space-above]captures a page, or one element with some page above it (useful when artwork leads into the component). Take the Jekyll original from the live site and the Astro version from the component stage (npx pnpm run previewinastro/, then/component-preview/<name>/).node $SK/measure.mjs <url> <width> <selector>...prints each element's box and the computed font, color, margins and padding, positioned relative to the first selector. Run it on both versions with equivalent selectors to check a match in numbers, not by eye.node $SK/compare.mjs <out.png> <width> <jekyll.png> <astro.png>puts the two screenshots side by side with captions.
Capture at least 1280px and 400px, and 800px when the layout has a middle state. Use the same widths for both sides.