Basecoat is a vanilla JavaScript/HTML/CSS port of shadcn/ui components. It reuses shadcn/ui's design patterns and Tailwind CSS classes while maintaining semantic HTML, native browser elements, and full accessibility (ARIA attributes, proper hierarchies, keyboard navigation). The goal is to provide shadcn-quality components without React dependencies.
Basecoat is not a literal DOM port of shadcn/ui. It is an alternative implementation with the same visual language and comparable component coverage, but with simpler markup, less JavaScript, less CSS, and no React/runtime primitive dependency.
-`basecoat.css` and the default CDN bundle remain Vega-compatible for Basecoat backward compatibility. This is a Basecoat compatibility decision; upstream shadcn/ui's current default generated config is Nova.
- Non-default styles must be selected through standalone style bundles (`basecoat-<style>.css`, `basecoat-<style>.cdn.css`, or docs `styles-<style>.css`). Do not load Vega/default first and then layer another style file on top.
- Split style files under `src/css/styles/style-*.css` are for source organization and advanced composition only. They must map upstream style intent directly and should not contain defensive resets whose only purpose is undoing another style pack.
- Do not assume existing Basecoat component CSS is still the right baseline. Reassess shared rules vs style-owned rules component by component.
- When Basecoat differs from upstream shadcn/ui, classify the difference as either intentional or drift, and prefer explicit decisions over silent divergence.
- CSS state selectors before JavaScript state when reliable
- small vanilla JS behavior only when native HTML/CSS cannot provide the interaction
- Keep public markup small and obvious. A user should be able to read a component example and understand the DOM without learning shadcn/ui internals.
- Use the least JavaScript that preserves expected behavior, accessibility, and keyboard support.
- Use the least CSS that preserves the visual contract and style-pack differences.
- Use the least markup that remains semantic and accessible.
- Prefer a single public class on the component root. Avoid requiring child classes when semantic elements, native controls, ARIA roles, or documented attributes can identify the child.
- Child selectors should usually target meaningful HTML (`input`, `textarea`, `select`, `button`, `kbd`, `svg`, `header`, `footer`, `label`) or accessibility semantics (`role`, `aria-*`) rather than invented slot classes.
- Do not create classes or DOM structures just because shadcn/ui has internal slots. Map upstream intent onto Basecoat's existing selectors first.
- If a new public class, wrapper, `data-*` attribute, or macro argument is needed, treat it as API design. Make it explicit and document it.
- Prefer direct semantic selectors where Basecoat already has a semantic structure:
- field labels: `.field > label`, not invented `.field-label`
- field groups: semantic `fieldset`, `[role='group']`, or `[role='radiogroup']`, not arbitrary wrappers unless documented
- native inputs/selects/textarea should remain actual native controls where possible
- Avoid purely decorative components when composition is enough. Document patterns instead of adding API surface.
- Keep generated or copied shadcn/ui implementation details out of Basecoat:
- no `cn-*` classes
- no `data-slot` dependency unless Basecoat explicitly owns that API
- no React/Base UI/Radix-specific DOM assumptions
- Prefer intentional divergence over awkward imitation. If upstream behavior does not map cleanly to native markup, document the Basecoat behavior and why.
## Markup and accessibility rules
- Start from the semantic element that matches the control:
- button action: `<button>`
- navigation/action link: `<a>`
- text input: `<input>`
- multiline input: `<textarea>`
- native select: `<select>`
- grouped form controls: `<fieldset>` + `<legend>` when appropriate
- dialog: native `<dialog>` when possible
- Add ARIA only when native semantics are insufficient or a custom interaction needs it.
- ARIA must reflect behavior, not styling. Do not add roles just to target CSS.
- Disabled items must be excluded from keyboard and pointer interactions where custom JS is involved.
- For composite widgets, keep keyboard behavior predictable:
- roving/highlight state should skip disabled items
- Escape closes popovers/dialogs where expected
- Arrow keys should match orientation
- Enter/Space behavior should match the control role
- RTL support should use logical properties/utilities by default. Use explicit `:dir(rtl)` only when logical utilities cannot express the behavior.
- Directional icons are not automatically flipped. Flip only icons whose meaning is directional.
## CSS architecture rules
- Shared component CSS in `src/css/components/*.css` should own structure, layout primitives, accessibility selectors, and behavior hooks.
- Style-pack CSS in `src/css/styles/style-*.css` should own visual choices:
- color
- radius
- shadow/ring
- typography
- spacing that is style-specific
- variant visuals
- state visuals that differ by style
- Do not put style-pack defaults in component CSS and then override them later. Each style bundle must stand on its own.
- Avoid defensive resets that exist only because another style was loaded first. Non-default style bundles should not depend on Vega/default being loaded.
- Use Tailwind logical utilities (`ps`, `pe`, `ms`, `me`, `start`, `end`, `rounded-s`, `rounded-e`) instead of physical left/right when possible.
- Prefer compact selectors with `:is()` and `:has()`, but only when they keep the public API clearer.
- Use `border` for separators and lines, especially `<hr>`/separator elements. Avoid fake separator backgrounds unless upstream behavior requires it and markup supports it.
- Keep visual rules close to the component style block. Do not scatter one component's style-pack rules across unrelated sections.
- When using pseudo-elements for native controls, keep the native control accessible and form-submittable.
- Avoid `!important`. Use it only when matching upstream behavior or browser-native styling makes it unavoidable, and keep the selector narrow.
## JavaScript rules
- JavaScript files are behavior only. Do not use JS to paper over CSS architecture issues.
- Every JS component must register with `window.basecoat.register()` and be safe to initialize multiple times.
- Use `data-*-initialized` flags for idempotence.
- Emit custom events for meaningful user actions when useful for integration.
- Keep generated DOM minimal and consistent with the documented HTML structure.
- Do not couple unrelated components through hidden assumptions. If Select and Combobox share behavior, factor it deliberately or keep their files separate when the UX differs.
- Prefer native browser APIs:
- native `<dialog>` before custom modal stacks
- native form controls before custom controls
- native events before synthetic abstractions
- If JS is needed only to sync visual state, keep it small and local. Example: `range.js` only syncs `--slider-value`.
## Basecoat vs upstream shadcn/ui
- Always check upstream before changing a migrated component:
- component source under `../_sandbox/shadcn-ui/apps/v4/registry/bases/base/ui/`
- docs under `../_sandbox/shadcn-ui/apps/v4/content/docs/components/`
- examples under `../_sandbox/shadcn-ui/apps/v4/registry/bases/base/examples/`
- all eight style packs under `../_sandbox/shadcn-ui/apps/v4/registry/styles/`
- Map upstream concepts, not upstream implementation details.
- When upstream supports behavior that does not map to Basecoat's native implementation, document the gap instead of faking it.
- Example: Basecoat Slider uses native single-thumb range inputs; upstream Slider supports multi-thumb and vertical orientation.
- Example: Basecoat Tooltip is CSS-only text content; upstream Tooltip supports portalled arbitrary content.
- Example: Basecoat Select/Combobox popovers remain inline-positioned rather than matching every portalled positioning behavior.
- Style parity matters, but not at the cost of worse Basecoat markup.
- If a discrepancy is found, classify it:
-`drift fixed`: Basecoat was simply out of date
-`intentional`: Basecoat differs because of semantic/native/vanilla constraints
-`deferred`: valid upstream feature, not implemented yet
## Documentation rules
- Component docs are part of the component API. Update docs whenever CSS, JS, templates, variants, supported states, or supported markup changes.
- Docs should be Basecoat docs, not copied React docs. Do not include useless React-specific API tables, `asChild`, or composition diagrams that do not match Basecoat markup.
- Prefer concrete HTML examples over abstract component trees.
- Each component page should generally include:
- frontmatter title/description/toc
- a top preview using the most common/default example
-`Usage`
- install/include notes for JS components
- a minimal HTML example
-`HTML structure` or class/API notes when the markup is not self-evident
- examples aligned with upstream where they apply
- Basecoat-specific differences and unsupported upstream features
- RTL example or an explicit note if RTL is deferred
- For JS components, include:
- required script tags (`basecoat.min.js` plus component file, unless included in all bundle)
- initialization expectations
- custom events when relevant
- config object shape when relevant
- macro links if Nunjucks/Jinja macros exist
- For CSS-only/native components, do not invent an API Reference section unless there is a real Basecoat API to document.
- Do not add "Composition" sections unless the component truly has a stable Basecoat composition pattern. If the section only mirrors shadcn/ui React internals, remove it.
- Examples should be realistic and named clearly:
-`Default`
-`Disabled`
-`Invalid`
-`Groups`
-`Scrollable`
-`RTL`
- style/variant/size examples when applicable
- If Basecoat intentionally differs from upstream, say it near the relevant usage/example section.
- Keep snippets minimal. Avoid giant copied examples unless the structure is genuinely important.
- For deprecated or removed APIs, update the page and `CHANGELOG.md`/migration notes rather than leaving stale examples.
- Empty/Item: Pure composition unless a future pass proves a root class is needed
- Input Group: Lightweight CSS component with a single `.input-group` root class, semantic children, optional `data-align`, and `data-control` for custom controls