NS Design System
v3.0.0 · 257 tokens

115Composer

The input. One hairline box that grows with the textarea rather than a fixed field with a scrollbar — a student pasting an Apex class needs to see it. Focus moves the ring to the whole composer, so the toolbar reads as part of the control it belongs to.

Use it for

  • The assistant's input, docked at the foot of the thread
  • The context pill for what is in scope — the lesson, the file, the org
  • Attachments: Apex classes, flow screenshots, error logs

Not for

  • A search field — a search box returns results, this one starts a conversation
  • Hiding the Enter/Shift+Enter contract: the wrong guess posts half a question to a class

Default

Enter to send · Shift+Enter for a new line
markup
<form class="ns-aicomposer">
  <label class="ns-visually-hidden" for="ai-doc-in">Ask the assistant</label>
  <textarea class="ns-aicomposer__area" id="ai-doc-in" rows="2" placeholder="Ask about this lesson, paste your Apex, or ask for a project…"></textarea>
  <div class="ns-aicomposer__bar">
    <button type="button" class="ns-aimode" aria-haspopup="true" aria-expanded="false"><i class="ph ph-graduation-cap" aria-hidden="true"></i> Tutor <i class="ph ph-caret-down" aria-hidden="true"></i></button>
    <button type="button" class="ns-btn ns-btn--quiet ns-btn--icon ns-btn--sm" aria-label="Attach a file"><i class="ph ph-folder-open" aria-hidden="true"></i></button>
    <button type="button" class="ns-btn ns-btn--quiet ns-btn--icon ns-btn--sm" aria-label="Dictate"><i class="ph ph-microphone" aria-hidden="true"></i></button>
    <span class="ns-aicomposer__spacer"></span>
    <span class="ns-aicomposer__hint">Enter to send · Shift+Enter for a new line</span>
    <button type="submit" class="ns-btn ns-btn--primary ns-btn--icon ns-btn--sm" aria-label="Send message"><i class="ph ph-paper-plane-tilt" aria-hidden="true"></i></button>
  </div>
</form>

Context & attachments

What the assistant can see is stated, not inferred. “Explain this bit” only means the paragraph in front of you if the lesson is genuinely in scope — and the student must be able to take it out.

Apex basics · lesson 03 AccountTrigger.cls4 KB error-log.txtuploading flow-screenshot.pngtoo large
3620 / 4000
markup
<form class="ns-aicomposer">
  <div class="ns-aifiles">
    <span class="ns-aicontext"><i class="ph ph-book-open-text" aria-hidden="true"></i> Apex basics · lesson 03
      <button type="button" class="ns-aicontext__x" aria-label="Remove this lesson from context"><i class="ph ph-x" aria-hidden="true"></i></button></span>
    <span class="ns-aifile"><i class="ph ph-file-text" aria-hidden="true"></i><span class="ns-aifile__name">AccountTrigger.cls</span><span class="ns-aifile__size">4 KB</span>
      <button type="button" class="ns-btn ns-btn--quiet ns-btn--icon ns-btn--xs" aria-label="Remove AccountTrigger.cls"><i class="ph ph-x" aria-hidden="true"></i></button></span>
    <span class="ns-aifile" data-state="uploading"><i class="ph ph-circle-notch" aria-hidden="true"></i><span class="ns-aifile__name">error-log.txt</span><span class="ns-aifile__size">uploading</span></span>
    <span class="ns-aifile" data-state="failed"><i class="ph ph-warning" aria-hidden="true"></i><span class="ns-aifile__name">flow-screenshot.png</span><span class="ns-aifile__size">too large</span></span>
  </div>
  <label class="ns-visually-hidden" for="ai-doc-in2">Ask the assistant</label>
  <textarea class="ns-aicomposer__area" id="ai-doc-in2" rows="2" placeholder="What is wrong with this trigger?"></textarea>
  <div class="ns-aicomposer__bar">
    <button type="button" class="ns-btn ns-btn--quiet ns-btn--icon ns-btn--sm" aria-label="Attach a file"><i class="ph ph-folder-open" aria-hidden="true"></i></button>
    <span class="ns-aicomposer__spacer"></span>
    <span class="ns-aicomposer__count" data-state="near">3620 / 4000</span>
    <button type="submit" class="ns-btn ns-btn--primary ns-btn--icon ns-btn--sm" aria-label="Send message"><i class="ph ph-paper-plane-tilt" aria-hidden="true"></i></button>
  </div>
</form>

Mode picker

Which teacher you are talking to. Tutor explains, Coach answers with a question first, Reviewer marks the code you paste. The trigger is mono because it is a label; the menu is the shared ns-menu.

Mode

markup
<div style="display:flex;gap:var(--space-4);align-items:flex-start">
  <button type="button" class="ns-aimode" aria-expanded="true" aria-haspopup="true"><i class="ph ph-graduation-cap" aria-hidden="true"></i> Tutor <i class="ph ph-caret-down" aria-hidden="true"></i></button>
  <div class="ns-menu" style="position:static;display:block;inline-size:17rem">
    <p class="ns-menu__label">Mode</p>
    <button type="button" class="ns-menu__item" aria-current="true"><i class="ph ph-graduation-cap" aria-hidden="true"></i> Tutor <span>explains, then shows</span></button>
    <button type="button" class="ns-menu__item"><i class="ph ph-question" aria-hidden="true"></i> Coach <span>asks you first</span></button>
    <button type="button" class="ns-menu__item"><i class="ph ph-code" aria-hidden="true"></i> Reviewer <span>marks your Apex</span></button>
  </div>
</div>

Signed out

Visible but disabled, with the reason on it. A composer that is simply gone reads as a broken page.

Sign in to send
markup
<div class="ns-aicomposer" aria-disabled="true">
  <label class="ns-visually-hidden" for="ai-doc-in3">Ask the assistant</label>
  <textarea class="ns-aicomposer__area" id="ai-doc-in3" rows="2" disabled placeholder="Sign in to ask a question…"></textarea>
  <div class="ns-aicomposer__bar">
    <span class="ns-aicomposer__hint"><i class="ph ph-lock-simple" aria-hidden="true"></i> Sign in to send</span>
    <span class="ns-aicomposer__spacer"></span>
    <button type="button" class="ns-btn ns-btn--primary ns-btn--sm">Sign in</button>
  </div>
</div>

Accessibility contract

  • The textarea has a real label, visually hidden — a placeholder is not a label
  • The focus ring is on the wrapper, so the whole control shows focus rather than an inner box
  • aria-disabled on the wrapper for the signed-out state, with the reason in the bar rather than a dead control