Overview
Kolibri’s frontend is built with Vue.js 2.7 and follows the Composition API pattern. This guide covers the essential patterns, components, and conventions you need to know.
Critical : Always use the Composition API (setup()) for new components. Do not use the Options API (data(), computed:, methods:).
Frontend Stack
Framework : Vue.js 2.7 with Composition API
State Management : Composables (Vuex is deprecated)
Styling : SCSS with RTLCSS for RTL support
Build Tool : Webpack
Design System : Kolibri Design System (KDS)
Testing : Jest + Vue Testing Library
Component Development
Component Reuse Hierarchy
Before creating a new component, always search for existing ones in this order:
Kolibri Design System (KDS)
Check the Design System catalog first. KDS provides KButton, KTextbox, KSelect, KModal, KCircularLoader, KIcon, and many more.
Core Kolibri Components
Browse packages/kolibri/components/ for components like CoreTable, AuthMessage, BottomAppBar, AppBar, etc.
Kolibri-Common Components
Check packages/kolibri-common/components/ for shared components like AccordionContainer, BaseToolbar, MetadataChips.
Create New Component
Only create a new component if none of the above provide what you need.
If an existing component does 80% of what you need, wrap it - don’t rewrite it.
Component Structure (Composition API)
All new Vue components must use the setup() function:
< template >
< div : style = " { backgroundColor: $themeTokens . surface } " >
< h1 > {{ title$ () }} </ h1 >
< p > {{ message }} </ p >
< KButton @ click = " handleClick " >
{{ buttonLabel$ () }}
</ KButton >
</ div >
</ template >
< script >
import { ref , computed } from 'vue' ;
import { createTranslator } from 'kolibri/utils/i18n' ;
import useUser from 'kolibri/composables/useUser' ;
const strings = createTranslator ( 'MyComponentStrings' , {
title: {
message: 'Welcome to Kolibri' ,
context: 'Page heading' ,
},
buttonLabel: {
message: 'Get started' ,
context: 'Button to begin' ,
},
});
export default {
name: 'MyComponent' ,
setup () {
// Composables
const { isLearner } = useUser ();
// Reactive state
const message = ref ( 'Hello' );
// Computed properties
const displayMessage = computed (() => {
return isLearner . value ? ` ${ message . value } , learner!` : message . value ;
});
// Methods
function handleClick () {
message . value = 'Button clicked!' ;
}
// Expose translations
const { title$ , buttonLabel$ } = strings ;
return {
// Reactive refs
message ,
displayMessage ,
// Methods
handleClick ,
// Translations
title$ ,
buttonLabel$ ,
};
} ,
} ;
</ script >
< style lang = "scss" scoped >
h1 {
font-size: 24px;
margin-bottom: 16px;
}
p {
color: $text-color;
padding-left: 8px; // Auto-flipped to padding-right in RTL
}
</ style >
Component name must match the filename. Use PascalCase for both.
State Management with Composables
Composables are the preferred way to share state and logic. Vuex is deprecated.
Using an Existing Composable
import { computed } from 'vue' ;
import useUser from 'kolibri/composables/useUser' ;
import useChannels from 'kolibri-common/composables/useChannels' ;
export default {
name: 'ChannelList' ,
setup () {
const { isAdmin } = useUser ();
const { channelsMap , fetchChannels } = useChannels ();
const canManageChannels = computed (() => isAdmin . value );
// Fetch channels on mount
fetchChannels ();
return {
channelsMap ,
canManageChannels ,
};
} ,
} ;
Creating a Composable
Composables follow the use* naming convention:
import { ref , computed } from 'vue' ;
/**
* A composable for managing a counter
*/
export default function useCounter ( initialValue = 0 ) {
// Reactive state
const count = ref ( initialValue );
// Computed properties
const doubled = computed (() => count . value * 2 );
// Methods
function increment () {
count . value ++ ;
}
function decrement () {
count . value -- ;
}
function reset () {
count . value = initialValue ;
}
// Return public API
return {
count ,
doubled ,
increment ,
decrement ,
reset ,
};
}
Shared State Pattern
For globally-shared state, define refs at the module level:
import { ref , reactive } from 'vue' ;
import { get , set } from '@vueuse/core' ;
import ChannelResource from 'kolibri-common/apiResources/ChannelResource' ;
// Module-level state - shared across all consumers
const channelsMap = reactive ({});
const localChannelsCache = ref ([]);
function fetchChannels ( params ) {
return ChannelResource . list ({ available: true , ... params }). then ( channels => {
for ( const channel of channels ) {
set ( channelsMap , channel . id , channel );
}
if ( Object . keys ( params ). length === 0 ) {
set ( localChannelsCache , channels );
}
return channels ;
});
}
export default function useChannels () {
return {
channelsMap , // Shared state
localChannelsCache , // Shared state
fetchChannels ,
};
}
Use module-level state sparingly. Only use it when multiple components truly need to access the same data.
See the Composables guide for more patterns.
Styling and Theming
Always Use Theme Tokens
Never use hard-coded color values. Always use $themeTokens and $themePalette.
< template >
< div : style = " {
color: $themeTokens . text ,
backgroundColor: $themeTokens . surface
} " >
< p : style = " { color: $themeTokens . annotation } " >
Secondary text
</ p >
</ div >
</ template >
Common theme tokens:
$themeTokens.text - Primary text color
$themeTokens.annotation - Secondary text color
$themeTokens.surface - Surface background
$themeTokens.primary - Primary brand color
$themeTokens.error - Error state color
For dynamic styles in computed properties, use $computedClass.
Style Blocks, Not Inline Styles
Use <style> blocks for non-dynamic styles. RTL support (RTLCSS) cannot flip inline styles .
<!-- ❌ Wrong: Inline styles don't flip for RTL -->
< template >
< div style = " padding-left : 16 px ; " >
Content
</ div >
</ template >
<!-- ✅ Correct: Style blocks auto-flip for RTL -->
< template >
< div class = "content" >
Content
</ div >
</ template >
< style scoped >
.content {
padding-left : 16 px ; /* Auto-flips to padding-right in RTL */
}
</ style >
For dynamic directional styles, use the isRtl property:
setup () {
function scrollForward () {
if ( this . isRtl ) {
scrollLeft ();
} else {
scrollRight ();
}
}
}
Responsive Design
Do not use CSS @media queries. Use the responsive-window or responsive-element system.
Kolibri runs on varied screen sizes including Android devices. The responsive system provides breakpoints:
import useKResponsiveWindow from 'kolibri-common/composables/useKResponsiveWindow' ;
export default {
setup () {
const { windowIsSmall , windowBreakpoint } = useKResponsiveWindow ();
return {
windowIsSmall ,
windowBreakpoint ,
};
} ,
} ;
API Calls with Resources
Use Resource classes for all API calls. Never use raw fetch or axios.
Define API Resources
Create resources in apiResources.js:
import { Resource } from 'kolibri/apiResource' ;
/**
* Gets Lessons assigned to the learner
*/
export const LearnerLessonResource = new Resource ({
name: 'learnerlesson' ,
namespace: 'kolibri.plugins.learn' ,
});
export const LearnerCourseResource = new Resource ({
name: 'learnercourse' ,
namespace: 'kolibri.plugins.learn' ,
// Custom method
async getResumeData ( id ) {
const response = await this . accessDetailEndpoint ( 'get' , 'resume' , id );
return response . data ;
},
});
Use Resources in Components
import { ref } from 'vue' ;
import { LearnerLessonResource } from '../apiResources' ;
export default {
setup () {
const lessons = ref ([]);
const loading = ref ( false );
async function loadLessons () {
loading . value = true ;
try {
lessons . value = await LearnerLessonResource . fetchCollection ();
} catch ( error ) {
console . error ( 'Failed to load lessons:' , error );
} finally {
loading . value = false ;
}
}
return {
lessons ,
loading ,
loadLessons ,
};
} ,
} ;
Internationalization (i18n)
All user-visible text must be internationalized. Never hard-code strings in templates.
Using createTranslator
import { createTranslator } from 'kolibri/utils/i18n' ;
const strings = createTranslator ( 'QuizStrings' , {
title: {
message: 'Quiz Results' ,
context: 'Page heading' ,
},
score: {
message: 'You scored {score} out of {total}' ,
context: 'Score display' ,
},
questionsLabel: {
message: '{count, plural, one {# question} other {# questions}}' ,
context: 'Number of questions' ,
},
});
export default {
setup () {
// Destructure with $ suffix for use in templates
const { title$ , score$ , questionsLabel$ } = strings ;
return {
title$ ,
score$ ,
questionsLabel$ ,
};
} ,
} ;
In template:
< template >
< div >
< h1 > {{ title$ () }} </ h1 >
< p > {{ score$ ({ score: 8 , total: 10 }) }} </ p >
< p > {{ questionsLabel$ ({ count: 5 }) }} </ p >
</ div >
</ template >
ICU Message Syntax
Use ICU syntax for plurals, variables, and formatting:
{
itemCount : {
message : '{count, plural, one {# item} other {# items}}' ,
context : 'Number of items' ,
},
greeting : {
message : 'Hello, {name}!' ,
context : 'User greeting' ,
},
}
See the Internationalization guide for complete details.
Icons
Always use the KIcon component from the Design System:
< template >
< KIcon icon = "add" />
< KIcon icon = "forward" /> <!-- Auto-flips in RTL -->
< KIconButton icon = "close" @ click = " handleClose " />
</ template >
Browse available icons at the Design System Icons page .
Common Patterns
Loading States
< template >
< div >
< KCircularLoader v-if = " loading " />
< div v-else >
< p v-for = " item in items " : key = " item . id " >
{{ item . name }}
</ p >
</ div >
</ div >
</ template >
< script >
import { ref } from 'vue' ;
export default {
setup () {
const loading = ref ( true );
const items = ref ([]);
async function loadData () {
loading . value = true ;
try {
items . value = await fetchItems ();
} finally {
loading . value = false ;
}
}
return { loading , items , loadData };
} ,
} ;
</ script >
< template >
< form @ submit . prevent = " handleSubmit " >
< KTextbox
v-model = " username "
: label = " usernameLabel$ () "
: invalid = " usernameError "
: invalidText = " usernameError "
/>
< KButton type = "submit" : disabled = " ! isValid " >
{{ submitLabel$ () }}
</ KButton >
</ form >
</ template >
< script >
import { ref , computed } from 'vue' ;
export default {
setup () {
const username = ref ( '' );
const usernameError = ref ( '' );
const isValid = computed (() => {
return username . value . length >= 3 ;
});
function handleSubmit () {
if ( ! isValid . value ) {
usernameError . value = 'Username must be at least 3 characters' ;
return ;
}
// Submit form
}
return {
username ,
usernameError ,
isValid ,
handleSubmit ,
};
} ,
} ;
</ script >
Best Practices
Use Composition API for all new code
New components must use setup(). Do not use Options API (data(), computed:, methods:).
Search for existing components first
Check KDS, kolibri/components/, and kolibri-common/components/ before creating new components.
Use composables, not Vuex
Vuex is deprecated. Use composables for state management.
Never hard-code colors. Use $themeTokens and $themePalette.
Internationalize all text
Use createTranslator for all user-visible strings.
Use Resource classes for API calls
Never use raw fetch or axios. Define resources in apiResources.js.
Style blocks for RTL support
Put non-dynamic styles in <style> blocks so RTLCSS can flip them for RTL languages.
Component name matches filename
Both should be PascalCase and identical.
Testing
Write tests for all components. See the Testing guide for details.
import { render , screen } from '@testing-library/vue' ;
import MyComponent from '../MyComponent.vue' ;
describe ( 'MyComponent' , () => {
it ( 'renders the title' , () => {
render ( MyComponent , {
props: { title: 'Test Title' },
});
expect ( screen . getByText ( 'Test Title' )). toBeTruthy ();
});
});
Next Steps
Kolibri Design System Explore all available components and patterns
Testing Guide Learn how to write frontend tests with Jest
Internationalization Deep dive into i18n workflow and patterns
Backend Development Understand the Django backend and API patterns