half-built-ui
About
This page shows every component in the half-built design system. The system was extracted from half-built-robots.com. That site uses these packages. All three packages are published on npm under the MIT license.
The palette editor in the corner changes the theme variables live. It also prints the current values as a block that can be pasted into a stylesheet. The block holds every theme value. Eight are brand colors and four are code viewer colors. Syntax highlighting reads the same variables, so the code and its background change with the palette.
npm install @half-built/css @half-built/astro @half-built/tooling Frame
The header and footer come from the package and contain no site-specific values. The site name and other identity details are passed in as typed props. The theme toggle is in the header, and a second copy is in the palette panel next to the color pickers. All toggles on a page stay in sync.
Show imports
import Masthead from "@half-built/astro/components/Masthead.astro";
import Footer from "@half-built/astro/components/Footer.astro";
import ThemeToggle from "@half-built/astro/components/ThemeToggle.astro";
import TwoColumn from "@half-built/astro/components/TwoColumn.astro"; Header title and search
The header's title slot replaces the text inside the
home link. The centering, the boxed tagline, and the heading level
stay with the package, and the slotted element's own class carries
any font change. With the slot empty, siteName
renders as plain text. Passing
searchAction={false} removes the search
button, flyout, and form from the header. This site does that,
because it has no search page.
---
import Masthead from "@half-built/astro/components/Masthead.astro";
---
<Masthead
siteName="Sample Site"
tagline="A sample tagline"
nav={nav}
socials={socials}
dateStr={dateStr}
searchAction={false}
>
<span slot="title" class="wordmark">Sample <em>Site</em></span>
</Masthead> Two-column layout
TwoColumn is the page row. It holds the main column and a sidebar. The sidebar takes 30% of the row and stacks under the main column at the sidebar breakpoint. The frame below stands in for the page.
This is the main column. It holds post bodies, archives and static pages. Its width is whatever the sidebar leaves.
A second placeholder paragraph gives the column some length next to the sidebar.
Cards
PostCard is the card used on archive pages. The second card here is marked as a draft. The draft badge lets an unfinished post be published while still labeled as unfinished. The third card has both corner badges. One marks an AI-generated image and the other marks a post with an interactive demo. CategoryCard is a smaller tile. It shows an image with the category name on a chip over its bottom edge.
A card's text is taken from the post body. In the Derived
excerpt example below, the text starts after an
ExcerptStart marker. The aside above the marker is
left out.
Show imports
import PostCard from "@half-built/astro/components/PostCard.astro";
import CategoryCard from "@half-built/astro/components/CategoryCard.astro";
import { deriveExcerpt } from "@half-built/astro/lib/excerpt.ts";
import ExcerptStart from "@half-built/astro/content/ExcerptStart.astro"; Post cards
Sample post, marked draft
Sample post, with corner badges
Derived excerpt
The sample post body is passed through deriveExcerpt
to produce the card's text. Text before the marker is not
included.
A short aside that opens the sample post and
should stay off the card.
<ExcerptStart />
The card text starts here, after the marker. It runs until the
excerpt limit, where it is cut at a word boundary with an ellipsis. Sample post, excerpt after a marker
Category cards
Components
This section covers the rest of the package, including
navigation and the blocks used in post bodies. Page-level
components are imported from components/ and post
body blocks from content/. The split only reflects
where the files live. No component in the system caps the width
of text. Text width comes from the layout's column.
Show imports
import Pagination from "@half-built/astro/components/Pagination.astro";
import PostNavigation from "@half-built/astro/components/PostNavigation.astro";
import Widget from "@half-built/astro/components/Widget.astro";
import LinkListWidget from "@half-built/astro/components/LinkListWidget.astro";
import Subscribe from "@half-built/astro/components/Subscribe.astro";
import Button from "@half-built/astro/content/Button.astro";
import Callout from "@half-built/astro/content/Callout.astro";
import EditorNote from "@half-built/astro/content/EditorNote.astro";
import WhenPublished from "@half-built/astro/content/WhenPublished.astro";
import PostLink from "@half-built/astro/content/PostLink.astro";
import Group from "@half-built/astro/content/Group.astro";
import CodeBlock from "@half-built/astro/content/CodeBlock.astro";
import Spacer from "@half-built/astro/content/Spacer.astro";
import Quote from "@half-built/astro/content/Quote.astro";
import Walkthrough from "@half-built/astro/content/Walkthrough.astro";
import Step from "@half-built/astro/content/Step.astro";
import Gallery from "@half-built/astro/content/Gallery.astro";
import GalleryImage from "@half-built/astro/content/GalleryImage.astro";
import BlogImage from "@half-built/astro/content/BlogImage.astro";
import MediaText from "@half-built/astro/content/MediaText.astro";
import Popout from "@half-built/astro/components/Popout.astro"; Pagination
Pagination is the numbered row at the bottom of an archive page. It renders nothing when there is only one page.
Post navigation
PostNavigation is the pair of links at the end of a post. The newer post is on the left and the older post is on the right. This matches the newest-first order of the archives.
Link list widget and Widget
Widget is a heading above arbitrary content. LinkListWidget is a Widget filled with a list of links and their counts.
Stat ledger
StatLedger sets a few figures side by side in one ruled box, a label over each value. One figure can take the accent. It is a description list, so a screen reader pairs each label with its value, and it stacks into one column at phone width.
- Sample A
- 1,024 units
- Sample B
- 512 units
- Ratio
- 50.0%
Meter
Meter shows how far a count has come toward its total, in ruled segments. It carries role="meter" with the value and a text reading, and it needs no script. The value is clamped to the range, so an overshoot fills the bar and a total of zero leaves it empty. A missing value, which is not a finite number, reads as empty too.
Popout
Popout holds a short note behind a cell in a data table. It is for
detail that is worth keeping but does not need its own column. On
a wide screen it opens below its trigger, or above it when there
is no room below. It closes automatically when the trigger scrolls
out of view, when keyboard focus leaves it, or when the window
crosses into phone width. On a phone it opens as a sheet from the
bottom of the screen, with the row name as its caption. The layout
loads popout.css and calls mountPopouts
once.
| Row | Value | Notes |
|---|---|---|
| Row one | 42 |
A note can contain a link and
inline code.
|
| Row two | 7 | Only one popout is open at a time. Opening this one closes the other. |
| Row three | 19 |
---
import "@half-built/css/popout.css";
---
<script>
import { mountPopouts } from "@half-built/astro/scripts/popout.ts";
mountPopouts(document);
</script> ---
import Popout from "@half-built/astro/components/Popout.astro";
---
<Popout label="Row one">
A note can contain a <a href="#components">link</a> and
<code>inline code</code>.
</Popout> Path player
The path player opens in the plate modal and adds playback controls and a timeline. The caller supplies the samples, the tracks under the stage, and the stage itself. The stage in this demo is a point moving on a closed curve. When reduced motion is set, the player opens paused.
---
import "@half-built/css/plate-modal.css";
import "@half-built/css/path-player.css";
---
<script>
import { createPathPlayer } from "@half-built/astro/scripts/path-player.ts";
const player = createPathPlayer(document, {
title, source, caption, duration, samples, tracks,
buildStage: (viewbox) => { /* append the stage, return true */ },
sink: { start, move, stop },
});
button.addEventListener("click", () => player.open(button));
</script> Plate frame
The plate frame is the plate modal's frame drawn inline: four
accent corners around a plate with the surface and shadow. An icon
box with .plate-frame-corner sits on the plate's
top-right corner, where the modal puts its close box, and can open
the full view. The corners come from the outer box's background,
so the markup adds no elements for them. At phone width the
corners drop and the box moves inside the plate, as in the modal.
<div class="plate-frame">
<div class="plate-frame-plate bracket-frame">
<button type="button" class="icon-box plate-frame-corner" aria-label="Enlarge sample">
<Icon name="maximize-2" size={16} strokeWidth={2} />
</button>
<!-- content -->
</div>
</div> Subscribe
Subscribe is a standard form with a method and an action, so it submits with JavaScript turned off. Its text is in named slots and can be replaced without forking the component. Its status lines can be replaced through the messages prop.
Join the Mailing List
If you want to hear when a new post or project goes up, leave an email and I'll send a note. No schedule, no spam.
Placeholder form. This demo does not send email. The action is not a real endpoint.
Button
Button has four variants and two sizes.
A single call to action uses the centered row.
Callout
Callout sets a passage apart from the body text. It is used for caveats and warnings the reader should not skip. The type sets the label and the line color.
Editor's note
EditorNote is a note to the author that only renders on a dev
server. A production build outputs nothing for it, so it can stay
in published content as a reminder. This one is forced visible
with the shown prop.
When published and Post link
Both components take the site's post collection as a prop and do
not read any content themselves. PostLink resolves a
slug to the post's dated URL at build time. While the post is a
draft, it renders plain text instead of a link.
WhenPublished renders its content only after the
named post is published. Its optional fallback slot
renders until then. The sample collection here has one published
post and one draft.
This passage is visible because its post is published. It links to the sample post. The URL was resolved from the slug.
This sentence names the sample draft in plain text because the post is still a draft. It becomes a link when the post is published.
This fallback stands in while the draft is unpublished.
Group
Group is a boxed prose container. It uses the same box as Callout without the label or tint.
Code block
def read_ir(code):
return "power" if code == 0x40BF else "unknown" Spacer
A fixed-height spacer sits below this paragraph.
Normal flow resumes here, after the spacer.
Quote
A quoted line, with an attribution footer.
Walkthrough and Step
- First step. One placeholder photo beside its text.
- Second step. The step number is on a chip in the corner of the image.
Gallery
Blog image
Media text
MediaText puts media on one side and prose on the other. This is the caption slot. It holds a short paragraph rather than a single line. On a phone the image stacks above the prose, which is centered like a figure caption.
CSS primitives
The tokens are the CSS variables that the rest of the system reads. Every color on this page comes from them. The palette editor changes the page by setting twelve of them. Each value is tuned to pass WCAG AA against its background. The prose and pattern sheets cover paragraph spacing and the boxed styles.
Show imports
import "@half-built/css/tokens";
import "@half-built/css/prose.css";
import "@half-built/css/patterns.css";
import Palette from "@half-built/astro/content/Palette.astro"; Table
Every table in the system uses this boxed style. Wide tables scroll inside their own box, so the page never scrolls sideways.
| Breakpoint | Query | What changes |
|---|---|---|
--bp-phone | max-width: 768px | The header's top band is hidden and the footer groups collapse. |
--bp-sidebar | max-width: 991px | The two-column row stacks. |
--bp-tablet | max-width: 1024px | Images load a file sized to the screen width instead of the 990px column. |
This wider table scrolls in its own box. It lists every color primitive in the sheet, with one row per family and one column per stop.
| Family | plain | 075 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 750 | 800 | 850 | 900 | vivid | a10 | a55 | a60 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| white | #ffffff | rgba(255, 255, 255, 0.1) | |||||||||||||||
| gray | #ececec | #efefef | #d9d9d9 | #cccccc | #9a9a9a | #555555 | #404040 | #3a3a3a | #2a2a2a | ||||||||
| black | #1a1a1a | #111111 | rgba(0, 0, 0, 0.1) | rgba(0, 0, 0, 0.6) | |||||||||||||
| graphite | rgba(48, 48, 48, 0.55) | ||||||||||||||||
| ink | #3b2e1e | #1b140c | |||||||||||||||
| parchment | #e8d9c3 | ||||||||||||||||
| brand-1 | #ffd18a | #ffaa3c | #d1820f | #a36300 | #e07c14 | ||||||||||||
| brand-2 | #8ee6f2 | #3cc7dd | #1a7f90 | ||||||||||||||
| red | #ff8a7a | #7a1f16 | |||||||||||||||
| violet | #b39ad6 | #7a5aa0 | |||||||||||||||
| umber | #8e7e67 |
Tokens
The token roles that components read, with the value of each in the current theme and palette.
| Swatch | Role | Hex | Used for |
|---|---|---|---|
| --ink | | Body text and rules | |
| --surface | | The page ground | |
| --rule | | Borders and frames | |
| --accent-1 | | Accent 1 lines and fills | |
| --accent-1-ink | | Accent 1 as readable text | |
| --accent-2 | | Accent 2 lines and fills | |
| --accent-2-ink | | Accent 2 as readable text | |
| --error-ink | | The error callout's line and label | |
| --code-token-function | | Function names in code | |
| --code-token-string | | Strings and constants in code |
Type scale
The fluid type sizes. Each line is set at its own size and shows the token name.
-
--font-size-xsSample text -
--font-size-smSample text -
--font-size-baseSample text -
--font-size-mdSample text -
--font-size-lgSample text -
--font-size-xlSample text -
--font-size-xxlSample text
Prose specimen
Placeholder body copy with inline code and a
placeholder link.
A placeholder blockquote in a gray box.
- Placeholder list item one
- Placeholder list item two
A sentence with a footnote reference.1 A claim with three sources stacked.2,3,4
Footnotes
Patterns
| Specimen | Class | Used for |
|---|---|---|
| Chip | .chip | Filled label. Used for the read-time chip and the category card's name. |
| Boxed label | .boxed-label | Bordered box, usually placed across a frame rule. Used for the header's date box and widget titles. |
.icon-box | Bordered square button. Used for social icons, the search button and the palette panel's close button. | |
| Micro label | .micro-label | Small bold text with wide letter spacing. Used for plate labels and the lightbox's HOME box. |
| Caption text, small and muted. | .caption | The small line under an image, beside media text or after a quote. |