# WebCraft > WebCraft publishes small interface elements one at a time: each is rendered live in the page, shown with the source you can take, and carries two notes — the caveat that breaks it, and what happens in an engine that does not support it. An element with no real caveat does not get published. > Note on this domain: web-craft.net previously hosted a Japanese web production business (2003-2016) and then an unrelated Australian freelance design practice trading as WebCraft (2021-2026). This publication is neither of them and claims nothing of theirs. Full disclosure at https://web-craft.net/about/. ## Elements - [Hover that stays off touchscreens](https://web-craft.net/elements/hover-that-stays-off-touchscreens/): `@media (hover: hover)`. The query tests the primary pointer only. A laptop with a touchscreen usually matches, since the trackpad counts as primary, and a finger tapping that same screen still latches :hover until the next tap. any-hover matches when any connected pointer can hover, which is usually too broad for styling. Test the primary pointer and accept that hybrid machines get the mouse version. - [The filled half of a range track](https://web-craft.net/elements/filled-half-of-a-range-track/): `::-moz-range-progress`. Only Firefox has a pseudo-element for the part of the track behind the thumb. In Chromium and Safari the fill is a trick: a wide box-shadow thrown left from the thumb and cut off by overflow: hidden on the input. That overflow clips everything outside the input's box, so the thumb can never stand taller than the track. A big round thumb on a hairline track needs one line of script instead, writing the value into a custom property that a gradient reads. - [Numbering the browser keeps for you](https://web-craft.net/elements/numbering-the-browser-keeps-for-you/): `counters()`. counter-reset creates a fresh counter scoped to the element, even when one with that name already exists above it. That is why the nested list above counts 2.1, 2.2 instead of 3, 4, and why a reset halfway down a list starts a nested copy where you expected the count to restart. Hiding changes the count as well: an item with display: none is skipped, one with visibility: hidden still counts, so two ways of hiding a step give two different numberings. The accessibility catch is in the markup. Numbers drawn by ::before are paint, and list-style: none makes VoiceOver stop announcing the list as a list, so keep the
pushes its track past its share and the whole grid overflows sideways — minmax(0, 1fr) is the fix and costs nothing.
- [Centring with no flex and no grid](https://web-craft.net/elements/centring-with-no-flex-and-no-grid/): `align-content (block)`. It distributes free space, so a box with none — an auto height, which is most boxes — accepts the declaration and does nothing with it. The height or min-height is the load-bearing half, and leaving it out is the whole of "align-content does not work in block layout". The other half is that only this property crossed over: align-items and justify-content are still inert in block layout, and reaching for them is the usual first guess. Children stay in the block flow while this happens, so their margins still collapse into each other and shift the centre by an amount you did not write.
- [An edge that fades instead of cutting](https://web-craft.net/elements/edge-that-fades-instead-of-cutting/): `mask-composite: intersect`. Two edges need two gradients, and the default mask-composite: add unions them — the union of "everything but the left" and "everything but the right" is everything, so both fades disappear and it reads as the property having done nothing. intersect is the whole fix. Then watch the shorthand: mask resets every longhand it does not name, mask-composite included, so a shorthand written after it silently throws the second gradient away. And a mask clips the element's entire painting, so the focus ring on the last chip fades out with the chip while the chip stays tabbable — invisible and fully interactive at the same time.
- [A vertical label that does not break the box](https://web-craft.net/elements/vertical-label-that-does-not-break-the-box/): `writing-mode: vertical-rl`. writing-mode moves the block axis and every physical property keeps pointing where it always did, so the layout stops agreeing with the names: height now caps how far the label runs down its own line and width sizes the block, which is why the first attempt either overflows the box or wraps after two words. The logical pair — inline-size, block-size, padding-inline, margin-block — are the ones that follow the rotation. Watch text-orientation: upright too: it stands latin letters up one at a time and turns off kerning and ligatures while it does.
- [A heading that sits on its own cap height](https://web-craft.net/elements/heading-that-sits-on-its-own-cap-height/): `text-box: trim-both`. It trims the block box, not every line: a heading that wraps keeps the full leading between its lines and only loses it above the first and below the last. The amount comes from the first available font's metrics, so a web font that arrives after the fallback drew the first frame trims by a different number and the heading shifts on load — the same jump size-adjust exists to fix. And the edge values are metric names rather than lengths: cap alphabetic cuts to the capital height and the baseline, which is what a display line wants, while text cuts to the font's own ascent and descent and barely moves anything.
- [Type that inverts itself over anything](https://web-craft.net/elements/type-that-inverts-itself-over-anything/): `mix-blend-mode: difference`. Blending only reaches as far as the nearest ancestor that forms an isolated group, and an opacity below 1, a transform, a filter, will-change or position: fixed quietly creates one — so the text inverts against a backdrop that is not the one on screen, and nothing in the styles says why. isolation: isolate is that same mechanism used on purpose: it pins the blend to a box you chose. The second trap is arithmetic, not support: difference against a mid grey returns another mid grey, so the type vanishes exactly where the backdrop is neither light nor dark.
- [An outline that does not eat the letters](https://web-craft.net/elements/outline-that-does-not-eat-the-letters/): `paint-order: stroke fill`. -webkit-text-stroke centres the stroke on the glyph outline, so half of a five-pixel stroke grows inward and thins the letterform until the counters in a, e and o close up. paint-order: stroke fill repaints the fill over the stroke and the shape comes back, which is the whole trick and the only fix that does not involve a second copy of the text. Two things to budget for: the stroke takes no space in layout, so it overflows the line box, crowds the line above and is cut by any overflow: hidden ancestor; and the property is still prefixed after fifteen years, with no unprefixed alias on the way.
- [A drop cap the browser measures for you](https://web-craft.net/elements/drop-cap-the-browser-measures-for-you/): `initial-letter`. initial-letter sizes and sinks the letter vertically and does nothing horizontally, so the text still runs flush against it until you add your own margin-right. Two numbers are also not one: initial-letter: 3 means three lines tall and sunk three, which drops the cap below the first line where you almost certainly wanted 3 2. It applies to ::first-letter in a block container and is ignored anywhere else, including on a flex item.
- [Stripes that stay right when rows are filtered](https://web-craft.net/elements/stripes-that-stay-right-when-rows-are-filtered/): `:nth-child(of S)`. li:nth-child(odd of :not(.gone)) and li:not(.gone):nth-child(odd) are different selectors, and the second one is the reading most people arrive at. The first numbers within the kept rows, which is what striping wants. The second numbers among all siblings and then throws away the ones that do not match, so hiding two rows leaves two stripes in a row and it looks like a rendering bug rather than a selector you wrote.
- [One declaration that covers both themes](https://web-craft.net/elements/one-declaration-that-covers-both-themes/): `light-dark()`. light-dark() does not look at the media query, it looks at the computed color-scheme of the element. Leave that unset and it silently returns the first value forever — the second swatch above is the same CSS with color-scheme: normal, and it never changes. A theme that half works is harder to find than one that does not work at all, and this is the usual reason for it.
- [Carousel dots the browser draws itself](https://web-craft.net/elements/carousel-dots-the-browser-draws-itself/): `::scroll-marker`. A marker only exists once content is set on it. An empty string counts; leaving the property out produces no pseudo-element at all, so the group renders as nothing and the feature reads as unsupported when it is working perfectly. The group also has to be declared on the scroll container itself with scroll-marker-group, not on the items.
- [A tooltip that follows its anchor with no JavaScript](https://web-craft.net/elements/tooltip-that-follows-its-anchor-with-no-javascript/): `anchor-name + position-area`. Without position-try-fallbacks the tooltip will happily render off the edge of the viewport, and you will not notice, because you are testing in a wide window with the anchor in the middle of it. The fallback list is not a polish step, it is the half of the feature that handles the anchor being near an edge — which is where tooltips actually live.
- [Styles that stop at a boundary you name](https://web-craft.net/elements/styles-that-stop-at-a-boundary-you-name/): `@scope`. @scope is not encapsulation. It limits where these rules apply and does nothing to stop rules from anywhere else reaching in — a stray p { color: red } in another sheet still wins on specificity and still paints the inner block. Reaching for it expecting shadow-DOM behaviour gets you the opposite of what you expected, quietly. The limit is also exclusive: to (.inner) leaves .inner itself outside the scope, so you cannot style the boundary element from within.
- [Animating height to auto, finally](https://web-craft.net/elements/animating-height-to-auto-finally/): `interpolate-size`. height: auto becomes animatable only after opting in with interpolate-size: allow-keywords — and the opt-in inherits. Put it on :root, as most examples do, and every intrinsic-size transition on the whole site changes behaviour in one line. Scope it to the component until that is a decision rather than an accident.
- [A spring in the easing function](https://web-craft.net/elements/spring-in-the-easing-function/): `linear()`. Both knobs leave together and both take 550ms; only the lower one arrives like an object with mass. linear() is not a curve — it is a polyline the engine walks between the stops you hand it, so a spring is a real spring equation sampled into two dozen of them. That is the first thing it costs: nobody writes this by hand, and a stop count low enough to read is low enough to see, as facets in what should be smooth. The second is that it is a timing function over a fixed duration and not physics, so it has no state. Toggle twice quickly and it does not carry its velocity into the new run the way a spring would; it restarts along the curve from wherever it stood, and the overshoot lands somewhere the eye reads as a fault. And the overshoot only exists on properties that can pass their end value: translate and transform can, opacity clamps at 1, so a bounce written on a fade is quietly just a fade.
- [Type that wraps to a curve, not a box](https://web-craft.net/elements/type-that-wraps-to-a-curve-not-a-box/): `shape-outside`. The lines follow the circle because the float carries a shape — and only because of that. shape-outside applies to floats and to nothing else, so the same declaration on a grid item, a flex item or a positioned box does exactly nothing, silently. What it does not do is change the element it sits on: that box is still a rectangle, its background still paints square, and the circle you see is border-radius doing separate work on the same numbers. Keep the two descriptions in step by hand or the text wraps a curve that is not there. The trap is margin: the shape resolves against the margin box by default, so adding margin: 10px grows the reference box and circle(50%) grows with it — the wrap drifts wider than the painted circle and the gap opens unevenly. Name the box and it stops moving: circle(50%) border-box pins the shape to the circle you drew, which is what lets the −5px margin here hang the disc past the text edge — a round form on the same vertical as flat type always reads a little inset — without dragging the wrap out with it. For a gap that follows the curve, use shape-margin, which is measured from the shape itself.
- [Reveal on scroll, with no observer](https://web-craft.net/elements/reveal-on-scroll-with-no-observer/): `animation-timeline: view()`. Put the hidden state in the keyframe’s from, never on the element itself. Engines without view() ignore the timeline, the animation runs its zero-second duration and finishes — content simply appears. Hide elements with opacity: 0 in their own styles instead and those engines show a blank page forever. That inversion is the entire safety of this pattern.
- [Clipping that leaves sticky alone](https://web-craft.net/elements/clipping-that-leaves-sticky-alone/): `overflow: clip`. Scroll both panes: the same heading sticks on the right and scrolls away on the left. overflow-x: hidden is not allowed to stay on its axis — the used value of the other one changes from visible to auto, so the wrapper becomes a scroll container. Every position: sticky below it now resolves against that box, which has no height of its own and therefore never scrolls, so it never sticks. This is what a wrapper added months earlier to stop one horizontal bleed does to a header nobody has touched. clip carries no such coupling: overflow-x: clip leaves overflow-y: visible alone.
- [A dropdown a keyboard can hold open](https://web-craft.net/elements/dropdown-a-keyboard-can-hold-open/): `:focus-within`. Hover-only menus lock keyboards out; :focus-within keeps the panel open while anything inside it holds focus, so Tab walks straight through. Two traps: the menu snaps shut the instant focus leaves, and a pixel of empty space between trigger and panel breaks the hover path — bridge the gap with padding on the panel, never margin. Hide it with visibility, not display, so the tab from trigger to first item has something to land in.
- [Avatars that never squash](https://web-craft.net/elements/avatars-that-never-squash/): `object-fit: cover`. The default is fill, which is why the left face is squashed: a wide photo forced into a square stretches. cover crops instead — but it crops from the centre, so a face near the edge of the photo needs object-position to stay in frame. And the box must have both dimensions: with only a width the height follows the image and there is nothing to crop against.
- [An underline that slides in](https://web-craft.net/elements/underline-that-slides-in/): `background-size`. text-decoration cannot animate its width, so the underline here is a background: a one-colour gradient sized 0% 2px, grown to 100%. The catch is that the percentage resolves against the whole inline box — on a link that wraps, each line fragment shows a slice of one long underline. Keep it for single-line labels and give wrapped prose links a plain text-decoration.
- [An analog clock wound by one line of JavaScript](https://web-craft.net/elements/analog-clock-wound-by-one-line-of-javascript/): `animation-delay: -43200s`. CSS cannot ask what time it is. The clock is three infinite rotations, and a negative animation-delay winds it: minus four hours starts an animation four hours in, instantly. The only JavaScript writes seconds-since-midnight into a custom property — and that is the whole script. The dial itself is laid out by cos() and sin(), not twelve hand-tuned offsets.
- [The logo that never hits the corner](https://web-craft.net/elements/logo-that-never-hits-the-corner/): `animation + cqw`. Two animations cannot share a property. Put the horizontal and the vertical drift on one element and the second translate wins outright instead of the two combining — which is why this needs one element per axis. And 100% in a translate is the element’s own size, so the travel is 100cqw - 100%: the box, less the logo.
- [resize keeps writing after max-width stops it](https://web-craft.net/elements/resize-keeps-writing-after-max-width-stops-it/): `resize + max-width`. Dragging writes an inline width and does not stop at max-width. The box paints clamped, but a fit-content parent measures the stored value instead, so anything centred in that parent drifts off the box it belongs to.
- [A gradient border that keeps its corners](https://web-craft.net/elements/gradient-border-that-keeps-its-corners/): `background + padding`. border-image ignores border-radius outright — square corners, every time, and no amount of radius fixes it. So the border is not a border here: it is padding over a gradient background.
- [A spotlight that follows the cursor](https://web-craft.net/elements/spotlight-that-follows-the-cursor/): `radial-gradient at var()`. Gate it behind @media (hover: hover) and (pointer: fine). On a touchscreen :hover latches after a tap, so the light switches on and stays on with nothing hovering it.
- [A progress bar tied to scroll, with no JavaScript](https://web-craft.net/elements/progress-bar-tied-to-scroll-with-no-javascript/): `animation-timeline: scroll()`. An animation whose timeline is unsupported does not simply switch off — it still applies its fill state. Ship this unguarded and browsers without scroll timelines show a permanently full progress bar.
- [A table header that stays put](https://web-craft.net/elements/table-header-that-stays-put/): `position: sticky`. Any ancestor carrying overflow: hidden switches sticky off silently — no warning, no error, the header just scrolls away. That ancestor is usually a card wrapper somebody added months later.
- [Cut a paragraph to three lines](https://web-craft.net/elements/cut-a-paragraph-to-three-lines/): `-webkit-line-clamp`. All four declarations are load-bearing. Bottom padding is the trap: the clipped line stays visible inside the padding box, so pad the parent, never the clamped element.
- [A textarea that grows as you type](https://web-craft.net/elements/textarea-that-grows-as-you-type/): `field-sizing: content`. Give it a max-height, or a pasted wall of text pushes the submit button off screen — worse than the scrollbar you were removing. lh units keep the bounds in lines, not guessed pixels.
- [An accordion with no JavaScript](https://web-craft.net/elements/accordion-with-no-javascript/): ``. Hiding the marker takes two declarations, not one. Miss either and one browser keeps a triangle you never designed for.
- [A focus ring for keyboards only](https://web-craft.net/elements/focus-ring-for-keyboards-only/): `:focus-visible`. :focus { outline: none } is the most common accessibility bug on the web, and it looks like a tidy-up. It takes the ring from keyboard users, the only people who needed it.
- [A wide table that keeps its first column](https://web-craft.net/elements/wide-table-that-keeps-its-first-column/): `position: sticky (inline)`. The corner cell is sticky in two directions at once, so it has to outrank both the header row and the pinned column. Give it the highest z-index of the three or the scrolling cells paint straight over it.
- [Shadows that show there is more to scroll](https://web-craft.net/elements/shadows-that-show-there-is-more-to-scroll/): `background-attachment: local`. The two cover gradients have to match the container background exactly. Put this on a transparent or patterned surface and the covers read as grey smears sitting on top of your design.
- [A segmented control with a sliding indicator](https://web-craft.net/elements/segmented-control-with-a-sliding-indicator/): `:has() + translate`. Never size the indicator in pixels. Hardcode a width and a shift and the pill lines up until someone renames an option — then it sits half over the next label, with no error to tell you. Equal columns and a shift of 100% of itself need no measuring.
- [A skeleton that does not lie about the layout](https://web-craft.net/elements/skeleton-that-does-not-lie-about-the-layout/): `@keyframes + gradient`. A skeleton whose blocks are not the size of the real content is worse than a spinner: the page visibly rearranges the moment data lands. Match the line heights and widths you will actually render.
- [A menu button that turns into a close button](https://web-craft.net/elements/menu-button-that-turns-into-a-close-button/): `aria-expanded`. It is a button with aria-expanded, not a checkbox. A checkbox announces itself as a checkbox, and a menu toggle that reads as "checkbox, not checked" is worse than no label at all.
- [A switch that is still a checkbox](https://web-craft.net/elements/switch-that-is-still-a-checkbox/): `appearance: none`. Style the input itself rather than hiding it behind a div. appearance: none strips the paint and keeps everything else — focus, the space key, the label association, the announced role.
- [A marquee that never jumps](https://web-craft.net/elements/marquee-that-never-jumps/): `translateX(-50%)`. The track holds the list twice and travels exactly -50%. Any other distance, or a single copy, and the loop visibly snaps back at the seam.
- [Frosted glass over anything](https://web-craft.net/elements/frosted-glass-over-anything/): `backdrop-filter`. The element needs its own translucent background. Fully opaque and there is nothing to see through; fully transparent and most engines skip the filter entirely. Somewhere near 40% is where it reads as glass.
- [Type painted with a gradient](https://web-craft.net/elements/type-painted-with-a-gradient/): `background-clip: text`. color: transparent is doing the work, so if the background fails to paint for any reason the text is invisible rather than merely unstyled. Never put a whole paragraph in it, only display type you can afford to lose.
- [A range slider you can actually style](https://web-craft.net/elements/range-slider-you-can-actually-style/): `::-webkit-slider-thumb`. The WebKit and Gecko thumb selectors cannot share a rule. One unknown pseudo-element invalidates the entire selector list, so a combined rule silently styles nothing anywhere. Write them out separately, every time.
- [A card that tips towards the cursor](https://web-craft.net/elements/card-that-tips-towards-the-cursor/): `perspective + rotate`. The perspective belongs on the parent, not on the card. Put it on the transformed element and every card gets its own vanishing point, so a row of them tilts in visibly different directions.
- [A gradient angle that actually animates](https://web-craft.net/elements/gradient-angle-that-actually-animates/): `@property`. Custom properties are strings until you register them. Animate an unregistered --angle and it jumps from start to end with nothing in between — no error, no warning, just a hard cut at 50%.
- [A card that reads its own container, not the window](https://web-craft.net/elements/card-that-reads-its-own-container-not-the-window/): `@container`. An element cannot query itself. The container-type goes on a wrapper and the query targets its children, so every queried component needs one box more than you expected.
- [Type that scales with its container](https://web-craft.net/elements/type-that-scales-with-its-container/): `cqi`. A cqi unit resolves against the nearest ancestor that declared a container, not the nearest ancestor you had in mind. Nest two containers and the inner one silently wins.
- [Boxes that keep their proportions](https://web-craft.net/elements/boxes-that-keep-their-proportions/): `aspect-ratio`. An explicit height beats aspect-ratio every time. The one that catches people is implicit: a flex or grid parent stretches its children by default, and that stretch is a height.
- [A heading that breaks where a typesetter would](https://web-craft.net/elements/heading-that-breaks-where-a-typesetter-would/): `text-wrap: balance`. Browsers stop balancing past roughly six lines and quietly do nothing beyond that. It is a headline tool: put it on body copy and you pay for the layout pass and get no result.
- [Paragraphs with no orphan last line](https://web-craft.net/elements/paragraphs-with-no-orphan-last-line/): `text-wrap: pretty`. This is the one for body copy, and balance is not. Swapping them is the common mistake: pretty only fixes the last lines, balance evens out every line but gives up on long text.
- [Hyphenation in a narrow column](https://web-craft.net/elements/hyphenation-in-a-narrow-column/): `hyphens: auto`. It does nothing without a lang attribute on the element or an ancestor. The browser needs to know which dictionary to hyphenate with, and silently declines rather than guessing.
- [An error state that waits its turn](https://web-craft.net/elements/error-state-that-waits-its-turn/): `:user-invalid`. :invalid matches from the moment the page loads, so an empty required field is red before anyone has typed a character. :user-invalid waits until the field has been interacted with.
- [Native controls in your palette](https://web-craft.net/elements/native-controls-in-your-palette/): `accent-color`. One property, one colour, no second opinion — the tick, the thumb and the fill are all derived from it. Need the check mark a different colour from the box and you are back to rebuilding the control.
- [A label that floats out of the way](https://web-craft.net/elements/label-that-floats-out-of-the-way/): `:placeholder-shown`. The trick needs a real placeholder attribute — even a single space — because the selector matches the placeholder being shown, not the value being empty. Drop the attribute and the label never moves.
- [A carousel the browser drives](https://web-craft.net/elements/carousel-the-browser-drives/): `scroll-snap-type`. scroll-snap-type goes on the scroller and scroll-snap-align on every child. Set only the first and nothing snaps, with no indication that half the recipe is missing.
- [Scrolling that stops at the edge](https://web-craft.net/elements/scrolling-that-stops-at-the-edge/): `overscroll-behavior`. This is the real fix for the page scrolling behind an open modal. Locking body with overflow: hidden throws away the reading position and jumps the layout by the scrollbar width.
- [Anchor links that clear a sticky header](https://web-craft.net/elements/anchor-links-that-clear-a-sticky-header/): `scroll-padding-top`. Jump to an anchor under a sticky header and the heading lands behind it. The offset belongs to the scroll container as scroll-padding-top, or to the target as scroll-margin-top, never to both.
- [A wrapper that knows its input is wrong](https://web-craft.net/elements/wrapper-that-knows-its-input-is-wrong/): `:has()`. Specificity is taken from the heaviest selector inside the brackets, not from :has() itself. Put an id in there and the whole rule inherits an id-level weight you did not intend to spend.
- [Hovering one row quiets the others](https://web-craft.net/elements/hovering-one-row-quiets-the-others/): `:has() + :not()`. This re-evaluates on every pointer move across the list. On a few dozen rows it is free; on a table of several hundred it is a visible frame cost, and the fix is to move the rule to the row rather than the list.
- [A layout that changes when the list gets long](https://web-craft.net/elements/layout-that-changes-when-the-list-gets-long/): `:has(:nth-child())`. A quantity query counts children, so it re-runs whenever the list changes. Keep the threshold to one breakpoint: chaining several counts makes the layout jump twice while items are still loading in.
- [A modal that traps focus for you](https://web-craft.net/elements/modal-that-traps-focus-for-you/): `showModal()`. show() and showModal() are different elements in practice. Only the modal one gets ::backdrop, makes the rest of the page inert, traps focus and closes on Escape. Call the wrong one and you have a floating box with none of it.
- [A popover with no JavaScript at all](https://web-craft.net/elements/popover-with-no-javascript-at-all/): `popovertarget`. It lives in the top layer, so z-index on it or on anything around it is meaningless. Stacking is decided by the order things were promoted, and that trips up anyone debugging it the usual way.
- [A section the page cannot reach](https://web-craft.net/elements/section-the-page-cannot-reach/): `inert`. inert removes the subtree from focus order and from the accessibility tree, which pointer-events: none and disabled do not. Do not add it around an open modal: showModal() already inerts the rest of the page, and doing both can strand focus.
- [Animating something in from display: none](https://web-craft.net/elements/animating-something-in-from-display-none/): `@starting-style`. There is nothing to transition from when an element appears: it had no previous style. @starting-style supplies that first frame. Without it the element simply snaps into place and the transition never runs.
- [The exit animation, which is the hard half](https://web-craft.net/elements/exit-animation-which-is-the-hard-half/): `allow-discrete`. display is a discrete property: it flips at the halfway point, so the element vanishes mid-fade. allow-discrete makes the browser hold display: none back until the rest of the transition has finished.
- [Reordering a list without it jumping](https://web-craft.net/elements/reordering-a-list-without-it-jumping/): `startViewTransition()`. Every element you want animated needs a view-transition-name, and each one has to be unique at the moment of the transition. Two matching names and the transition is skipped outright, with an error only in the console.
## Tools
- [Colour palette](https://web-craft.net/palette/): pick one colour and get its analogous, complementary, split-complementary and triad hues plus tint, shade and neutral ramps, all derived in CSS with `oklch(from var(--base) ...)` relative colour syntax. Every swatch is measured after gamut mapping and labelled with whether black or white text clears WCAG AA on it, which a colour wheel does not tell you.
## Tags
- [a11y](https://web-craft.net/tags/a11y/)
- [control](https://web-craft.net/tags/control/)
- [dialog](https://web-craft.net/tags/dialog/)
- [disclosure](https://web-craft.net/tags/disclosure/)
- [form](https://web-craft.net/tags/form/)
- [hover](https://web-craft.net/tags/hover/)
- [layout](https://web-craft.net/tags/layout/)
- [loading](https://web-craft.net/tags/loading/)
- [media](https://web-craft.net/tags/media/)
- [menu](https://web-craft.net/tags/menu/)
- [motion](https://web-craft.net/tags/motion/)
- [scroll](https://web-craft.net/tags/scroll/)
- [state](https://web-craft.net/tags/state/)
- [surface](https://web-craft.net/tags/surface/)
- [table](https://web-craft.net/tags/table/)
- [text](https://web-craft.net/tags/text/)
- [type](https://web-craft.net/tags/type/)
## Key facts
- Focus: CSS and HTML interface elements, their source, and where each one breaks.
- 81 elements published. Founded 2026-08-17.
- Every demo is live markup running in the page, not a screenshot.
- No advertising, affiliate links, or sponsored content.
- Catalogue: https://web-craft.net/elements/
- Feed: https://web-craft.net/feed.xml
- Contact: hello@web-craft.net