Documentation - CoreUI
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.
- Already covered? If a sanctioned wrapper / replacement already exists (
DropdownMenu,DatePicker,DateRangePicker,SearchableSelect,SsrTooltip,custom-tabs) — use it and stop. - Restricted? Check the What we don’t use rules (e.g.
CIcon→BaseIcon). - Import per-file:
import { CFoo } from '@coreui/vue-pro'— never register a global plugin. - ⭐ 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. - 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:. - Finalise: verify in light + dark and under the DemoConfigurator.
If the SSR test fails
Don’t reflexively reach for <ClientOnly> — decide in order:
- 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.
- 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#fallbackso the layout doesn’t shift. — e.g.DatePicker,DateRangePicker.
- No default-visible UI (dropdown menu, popover, modal body): wrap only the hidden subtree in
- 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 anidprop, or move awindow/documentread intoonMounted. - Or build a custom SSR-safe replacement when the non-determinism is baked in and unreachable from props. — e.g.
SsrTooltip.
- Fix the root cause so the render is deterministic — e.g. pass
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 use | Use instead | Why |
|---|---|---|
CIconforbidden — 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 | DropdownMenu | Raw 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 / DateRangePicker | Raw 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-tabs | When 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 modalin-modal only | SearchableSelect | CModal’s CFocusTrap yanks focus from teleported dropdowns. SearchableSelect is from-scratch (@floating-ui), no CoreUI. (CFormSelect is fine outside modals.) §1.2. |
CoreUI tooltip / popoverv-c-tooltip / v-c-popover | SsrTooltip | Both 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-pro | Standardised 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 partials | CoreUI 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
<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
<CAlert color="primary">Heads up — a primary alert.</CAlert>
<CAlert color="success">Saved successfully.</CAlert>CBadge
<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
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
<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

<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
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
<CProgress>
<CProgressBar :value="65">65%</CProgressBar>
</CProgress>CContainer / CRow / CCol
<CContainer>
<CRow class="g-2">
<CCol :md="6">.col-md-6</CCol>
<CCol :md="6">.col-md-6</CCol>
</CRow>
</CContainer>CSpinner
<CSpinner />
<CSpinner variant="grow" color="primary" />CTable + Head / Body / Row / Cells
| # | Name |
|---|---|
| 1 | Mark |
| 2 | Jacob |
<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
<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>