CoreUI in this project

The short version of .github/coreui.instructions.md — read that file for the full rationale, the SCSS mechanism, and the adoption flow.

Behaviour from CoreUI, looks from Limitless

We use @coreui/vue-pro for component behaviour only (state, focus-trap, accessibility wiring). The visual layer is Limitless (a Bootstrap 5 fork). CoreUI components inherit their look automatically because they render the same Bootstrap class names (.btn, .modal, .form-select) and the same unprefixed CSS custom properties (--primary, not --cui-primary) that Limitless already styles.

We never load CoreUI’s full CSS

Loading coreui.min.css would put two complete Bootstrap forks in conflict and break Limitless. For a Pro-exclusive widget Limitless can’t paint (e.g. the date picker), we cherry-pick just that widget’s SCSS partial into @layer coreui with $prefix:'' — unlayered Limitless then beats any layered rule, and CoreUI only fills the gaps.

No global plugin

CoreUI is not registered via app.use(). Import each component per file: import { CButton } from '@coreui/vue-pro'.

SSR is the gate ⭐

This is an Inertia + Vue 3 + Laravel SSR app. The single most important check before adopting any CoreUI component is the SSR hydration test. Pro widgets are the main risk — their initial render can be non-deterministic in a few distinct ways: random IDs (tooltip / popover), reading window/document at mount (dropdown menu), or rendering from the current date / locale / viewport (date pickers) — any of which makes the server render diverge from the client. When a component mismatches and its content isn’t SSR-relevant, wrap it in <ClientOnly> (e.g. DatePicker, DateRangePicker, DropdownMenu); if the content must be server-rendered, fix the root cause or build an SSR-safe replacement (e.g. SsrTooltip).

Adding a new CoreUI component

The flow for reaching for any CoreUI component the app doesn’t already use — free or Pro. Full version in .github/coreui.instructions.md §3.

  1. Already covered? If a sanctioned wrapper / replacement already exists (DropdownMenu, DatePicker, DateRangePicker, SearchableSelect, SsrTooltip, custom-tabs) — use it and stop.
  2. Restricted? Check the What we don’t use rules (e.g. CIconBaseIcon).
  3. Import per-file: import { CFoo } from '@coreui/vue-pro' — never register a global plugin.
  4. ⭐ Run the SSR hydration test — the single most important gate. Build client + SSR, run the crawler, and check the component’s page for Hydration … mismatch. If it mismatches, follow the procedure below.
  5. Styling: if Limitless already paints it (shared Bootstrap classes) there’s nothing to add; a Pro-exclusive class Limitless can’t paint → cherry-pick its SCSS partial into @layer coreui; a tweak on top → an app-SCSS override tagged // COREui:.
  6. Finalise: verify in light + dark and under the DemoConfigurator.
If the SSR test fails

Don’t reflexively reach for <ClientOnly> — decide in order:

  1. Is the content SSR-relevant? Must it be in the server HTML — SEO/crawler-visible, above-the-fold (LCP), or shown before JS hydrates? An interactive widget’s chrome usually isn’t; body / marketing content usually is. It’s a product call — ask the dev / PO when unsure.
  2. Not SSR-relevant → render client-only via a wrapper, shape depending on whether it shows UI by default:
    • No default-visible UI (dropdown menu, popover, modal body): wrap only the hidden subtree in <ClientOnly>; the trigger stays SSR’d; no fallback. — e.g. DropdownMenu.
    • Has default-visible UI (a picker input): wrap the whole component + a .form-control #fallback so the layout doesn’t shift. — e.g. DatePicker, DateRangePicker.
  3. SSR-relevant → it must stay server-rendered (<ClientOnly> is off the table):
    • Fix the root cause so the render is deterministic — e.g. pass :id="useId()" if it exposes an id prop, or move a window/document read into onMounted.
    • Or build a custom SSR-safe replacement when the non-determinism is baked in and unreachable from props. — e.g. SsrTooltip.

What we don’t use

CoreUI components we don’t reach for directly — either forbidden, or replaced by a custom component / wrapper — plus the packages and CSS we never load. What to use instead:

Don’t useUse insteadWhy
CIcon
forbidden — packages not installed
BaseIcon (Phosphor)The one hard prohibition (§1.3). Icons are self-hosted Phosphor and the CoreUI icon packages aren’t installed. The CIcon import is left commented in BaseIcon/index.vue as a deliberate marker.
CDropdownMenu (raw)
use the wrapper
DropdownMenuRaw CDropdownMenu reads window/document at mount → SSR hydration mismatch. DropdownMenu wraps it in <ClientOnly> (the outer CDropdown/CDropdownToggle stay SSR-safe). §1.2.
CDatePicker / CDateRangePicker (raw)
use the wrappers
DatePicker / DateRangePickerRaw Pro pickers render from the current date/locale/viewport (new Date() calendar default + toLocaleDateString(locale) + window.innerWidth) → server & client diverge → SSR hydration mismatch. Not random IDs (that's tooltip/popover). Always go through the ClientOnly wrappers — live examples in Pro components below. §1.2.
CTabs (for bespoke nav)
partially replaced
custom-tabsWhen you need the bespoke nav styling/state, use the from-scratch Tab / TabList / TabContent / TabPanel. (CTabs is still used directly elsewhere where its plain behaviour is fine.) §1.2.
A CoreUI dropdown / select inside a modal
in-modal only
SearchableSelectCModal’s CFocusTrap yanks focus from teleported dropdowns. SearchableSelect is from-scratch (@floating-ui), no CoreUI. (CFormSelect is fine outside modals.) §1.2.
CoreUI tooltip / popover
v-c-tooltip / v-c-popover
SsrTooltipBoth read CoreUI’s useUniqueId (Math.random) for their IDs, so the server and first client render diverge → SSR hydration drift. SsrTooltip is a from-scratch, SSR-safe replacement (manual positioning + Teleport). §1.2.
@coreui/vue / @coreui/coreui (free)
not imported
@coreui/vue-proStandardised on Pro, which re-exports the full free API — one package covers every component. The free @coreui/coreui is present only as a transitive dependency; nothing imports it. §1.1.
coreui.min.css (the full CSS bundle)
never loaded
Limitless + cherry-picked @layer coreui partialsCoreUI and Limitless are both full Bootstrap-5 forks; loading both makes two copies of every component fight in the cascade and dumps --cui-* vars into :root. We load only the Pro-exclusive widget partials we use, inside @layer coreui (unlayered Limitless wins). §2.1 / §2.5.

Pro components we use

Only two CoreUI Pro-exclusive components are in use: CDatePicker and CDateRangePicker. Both are used only through a custom CoreEngine wrapper — here’s why.

Why a custom wrapper?

The raw Pro pickers fail the SSR hydration test — but not because of random IDs (that's the tooltip/popover cause). Their calendar defaults to new Date(), they format dates with toLocaleDateString(locale), and they read window.innerWidth (isMobile) — so the server render (its date / timezone / locale, no viewport) diverges from the first client render. Vue then logs Hydration … mismatch and can leave the widget in a broken state.

The wrappers DatePicker / DateRangePicker (@coreModule/components/…) fix this by rendering the Pro component inside <ClientOnly>, so it mounts after hydration — there is no server markup left to mismatch. A .form-control-shaped #fallback holds the layout until it mounts (no shift), and the wrapper is fully transparent — every prop, v-model, event and slot is forwarded — so you use it exactly like the raw component.

Same pattern as DropdownMenu (which wraps CDropdownMenu); the full decision flow for any mismatching component lives in .github/coreui.instructions.md. Never import CDatePicker / CDateRangePicker directly — always go through the wrappers. The live previews below render through them.

CDatePickerPro

Use via DatePicker (@coreModule/components/DatePicker.vue). Add the timepicker prop for datetime mode.

<DatePicker placeholder="Select a date" />
<DatePicker timepicker placeholder="Select date & time" />
CDateRangePickerPro

Use via DateRangePicker (@coreModule/components/DateRangePicker.vue). Reuses the date-picker + calendar styling (range highlighting) — needs no extra SCSS.

<DateRangePicker />

Standard components we use

The top-level non-Pro CoreUI components, each with a live example + code. Pure sub-parts (CCardBody, CTableRow, CModalHeader…) aren’t listed separately — they appear inside their parent’s example. All are painted by Limitless via shared Bootstrap classes: no CoreUI CSS and (unlike the Pro pickers) no SSR wrapper needed. rule marks a component used directly but with a gotcha (What we don’t use / coreui.instructions.md §1.3).

CAccordion
First panel.
Second panel.
<CAccordion>
  <CAccordionItem :item-key="1">
    <CAccordionHeader>Section 1</CAccordionHeader>
    <CAccordionBody>First panel.</CAccordionBody>
  </CAccordionItem>
  <CAccordionItem :item-key="2">
    <CAccordionHeader>Section 2</CAccordionHeader>
    <CAccordionBody>Second panel.</CAccordionBody>
  </CAccordionItem>
</CAccordion>
CAlert
Heads up — a primary alert.
Saved successfully.
<CAlert color="primary">Heads up — a primary alert.</CAlert>
<CAlert color="success">Saved successfully.</CAlert>
CBadge
New3OK
<CBadge color="primary">New</CBadge>
<CBadge color="danger">3</CBadge>
CButton
<CButton color="primary">Primary</CButton>
<CButton color="secondary" variant="outline">Outline</CButton>
<CButton color="primary" variant="ghost">Ghost</CButton>
CButtonGroup
<CButtonGroup>
  <CButton color="primary" variant="outline">Left</CButton>
  <CButton color="primary" variant="outline">Mid</CButton>
  <CButton color="primary" variant="outline">Right</CButton>
</CButtonGroup>
CCard + Header / Body / Title / Text / Footer
Header
Card title

Body content goes here.

<CCard>
  <CCardHeader>Header</CCardHeader>
  <CCardBody>
    <CCardTitle>Card title</CCardTitle>
    <CCardText>Body content goes here.</CCardText>
    <CButton color="primary" size="sm">Action</CButton>
  </CCardBody>
  <CCardFooter class="text-muted small">Footer</CCardFooter>
</CCard>
CCloseButton
<CCloseButton />
CCollapse
Collapsible content.
<CButton color="primary" @click="visible = !visible">Toggle</CButton>
<CCollapse :visible="visible">
  <CCard class="mt-2"><CCardBody>Collapsible content.</CCardBody></CCard>
</CCollapse>
CDropdownno @click.stop on items

Renders the menu through the DropdownMenu wrapper — raw CDropdownMenu is SSR-unsafe (§1.2).

<CDropdown>
  <CDropdownToggle color="primary">Menu</CDropdownToggle>
  <!-- DropdownMenu = SSR-safe wrapper around CDropdownMenu (§1.2) -->
  <DropdownMenu>
    <CDropdownHeader>Header</CDropdownHeader>
    <CDropdownItem as="a" href="#">Action</CDropdownItem>
    <CDropdownItem as="a" href="#">Another</CDropdownItem>
    <CDropdownDivider />
    <CDropdownItem as="a" href="#">Separated</CDropdownItem>
  </DropdownMenu>
</CDropdown>
CForm + CFormLabel / CFormInput / CFormFeedback
<CForm>
  <CFormLabel>Email</CFormLabel>
  <CFormInput type="email" placeholder="[email protected]" />
  <CFormFeedback invalid>Please enter a valid email.</CFormFeedback>
</CForm>
CFormCheck
<CFormCheck label="Checkbox" />
<CFormCheck type="radio" name="r" label="Radio" />
CFormSelect
<CFormSelect :options="['One', 'Two', 'Three']" />
CFormSwitch
<CFormSwitch label="Enable" />
CFormTextarea
<CFormTextarea :rows="3" placeholder="Message" />
CImage
example
<CImage rounded thumbnail :width="96" src="…/cup-gold.webp" />
CInputGroup + CInputGroupText
@
<CInputGroup>
  <CInputGroupText>@</CInputGroupText>
  <CFormInput placeholder="Username" />
</CInputGroup>
CListGroup + CListGroupItem
  • First item
  • Active item
  • Third item
<CListGroup>
  <CListGroupItem>First item</CListGroupItem>
  <CListGroupItem active>Active item</CListGroupItem>
  <CListGroupItem>Third item</CListGroupItem>
</CListGroup>
CModaltabindex="-1" required
<CButton color="primary" @click="visible = true">Open modal</CButton>
<CModal tabindex="-1" :visible="visible" @close="visible = false">
  <CModalHeader><CModalTitle>Title</CModalTitle></CModalHeader>
  <CModalBody>Modal body.</CModalBody>
  <CModalFooter>
    <CButton color="secondary" @click="visible = false">Close</CButton>
  </CModalFooter>
</CModal>
CNav + CNavItem / CNavLink
<CNav variant="tabs">
  <CNavItem><CNavLink active href="#">Active</CNavLink></CNavItem>
  <CNavItem><CNavLink href="#">Link</CNavLink></CNavItem>
</CNav>
CNavbar + CNavbarNav / CNavbarText / CNavbarToggler
<CNavbar color-scheme="light" class="bg-light">
  <CNavbarToggler />
  <CNavbarNav>
    <CNavItem><CNavLink active href="#">Home</CNavLink></CNavItem>
  </CNavbarNav>
  <CNavbarText>Subtitle</CNavbarText>
</CNavbar>
COffcanvas + Header / Body / Title
<CButton color="primary" @click="visible = true">Open offcanvas</CButton>
<COffcanvas placement="end" :visible="visible" @hide="visible = false">
  <COffcanvasHeader><COffcanvasTitle>Title</COffcanvasTitle></COffcanvasHeader>
  <COffcanvasBody>Offcanvas body.</COffcanvasBody>
</COffcanvas>
CProgress + CProgressBar
65%
<CProgress>
  <CProgressBar :value="65">65%</CProgressBar>
</CProgress>
CContainer / CRow / CCol
.col-md-6
.col-md-6
<CContainer>
  <CRow class="g-2">
    <CCol :md="6">.col-md-6</CCol>
    <CCol :md="6">.col-md-6</CCol>
  </CRow>
</CContainer>
CSpinner
Loading...
Loading...
<CSpinner />
<CSpinner variant="grow" color="primary" />
CTable + Head / Body / Row / Cells
#Name
1Mark
2Jacob
<CTable>
  <CTableHead>
    <CTableRow>
      <CTableHeaderCell>#</CTableHeaderCell>
      <CTableHeaderCell>Name</CTableHeaderCell>
    </CTableRow>
  </CTableHead>
  <CTableBody>
    <CTableRow>
      <CTableDataCell>1</CTableDataCell>
      <CTableDataCell>Mark</CTableDataCell>
    </CTableRow>
  </CTableBody>
</CTable>
CTabs + CTabList / CTab / CTabContent / CTabPanel
First panel.
<CTabs :active-item-key="1">
  <CTabList variant="tabs">
    <CTab :item-key="1">First</CTab>
    <CTab :item-key="2">Second</CTab>
  </CTabList>
  <CTabContent>
    <CTabPanel :item-key="1">First panel.</CTabPanel>
    <CTabPanel :item-key="2">Second panel.</CTabPanel>
  </CTabContent>
</CTabs>
CToast + CToastBody / CToastClose
<CToast :autohide="false" visible>
  <div class="d-flex">
    <CToastBody>Hello, this is a toast.</CToastBody>
    <CToastClose class="me-2 m-auto" />
  </div>
</CToast>