Skip to content

Headless directives

Your markup, Blasdoc behaviour: tabs, copy, mode and keyboard.

@blasdoc/components holds behaviour with no opinion about pixels: selection, the keyboard contract, ARIA wiring, the clipboard and highlight status. Apply the directives to your own markup and you get all of it.

A code block that is entirely yours

ts
import {
  BlasdocCodeBlock,
  BlasdocCodeBlockState,
  BlasdocCopy,
  BlasdocTab,
  BlasdocTabList,
  BlasdocTabPanel,
} from '@blasdoc/components';
 
@Component({
  selector: 'my-snippet',
  imports: [BlasdocTabList, BlasdocTab, BlasdocTabPanel, BlasdocCopy],
  hostDirectives: [BlasdocCodeBlock],
  template: `
    <figure class="my-snippet">
      <figcaption>
        <div blasdocTabList aria-label="Package manager">
          @for (tab of state.tabs(); track tab.id) {
            <button [blasdocTab]="tab.id">{{ tab.label }}</button>
          }
        </div>
        <button blasdocCopy blasdocCopyLabel="Copy this snippet">Copy</button>
      </figcaption>
 
      <div blasdocTabPanel>
        <div #surface></div>
      </div>
    </figure>
  `,
})
export class MySnippet {
  protected readonly state = new BlasdocCodeBlockState({ tabs: [...] });
  private readonly block = inject(BlasdocCodeBlock, { self: true });
 
  constructor() {
    this.block.use(this.state);
  }
}

Nothing above comes from the default theme. What comes from Blasdoc is the behaviour.

The directives

DirectiveWhat it gives you
blasdocCodeBlockthe host; holds the state every other directive reads
blasdocTabListrole="tablist", arrow / Home / End keys, roving focus
blasdocTabrole="tab", aria-selected, aria-controls, tabindex
blasdocTabPanelrole="tabpanel", labelled by the active tab
blasdocCopycopies the active tab, sets data-copied, accessible name
blasdocModetoggles between preview and code

A .blasdoc-tab-active class marks the selected tab so you can style it; the look is entirely yours.

The state

ts
interface BlasdocCodeBlockState {
  readonly tabs: Signal<readonly BlasdocCodeTab[]>;
  readonly activeTab: Signal<BlasdocCodeTab | null>;
  readonly mode: Signal<'preview' | 'code'>;
  readonly status: Signal<'idle' | 'loading' | 'ready' | 'failed'>;
  readonly copied: Signal<boolean>;
 
  selectTab(id: string): void;
  setMode(mode: 'preview' | 'code'): void;
  handleKey(key: string): boolean;
  copy(): Promise<void>;
}