In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<template>
<main>
<h1 id="title">RECIPE BOX</h1>
<section id="actions">
<select id="recipes" size="3" v-model="SELECTED" v-show="RECIPES_NAMES.length">
<option v-for="name in RECIPES_NAMES">{{ name }}</option>
</select>
<div class="actions">
<button type="button" class="bi bi-trash3-fill" @click="deleteRecipe" title="Delete Recipe" :disabled="!RECIPES_NAMES.length" />
<button type="button" class="bi bi-pencil-fill" @click="openModal(SELECTED, [...RECIPES[SELECTED].ingredients], [...RECIPES[SELECTED].directions], 'edit')" title="Edit Recipe" :disabled="!SELECTED" />
<button type="button" class="bi bi-plus-lg" @click="openModal()" title="Create Recipe" />
<button type="button" class="bi bi-arrow-clockwise" @click="resetRecipes" title="Reset Recipes" />
</div>
</section>
<section class="box" v-for="(arr, name) in RECIPES[SELECTED]" v-show="arr.length > 0">
<h2 class="section-title">{{ name }}</h2>
<component :is="name == 'directions' ? 'ol' : 'ul'" :class="name">
<li v-for="i in arr">{{ i }}</li>
</component>
</section>
</main>
<Transition>
<aside id="modal" v-show="MODAL_OPENED" ref="MODAL_WINDOW">
<form id="modal-content" @submit.prevent="handleForm">
<div class="box">
<h2 class="section-title">Recipe</h2>
<input type="text" ref="RECIPE_NAME_INPUT" v-model="FORM.recipe" placeholder="Recipe Name" required>
</div>
<div class="box" v-for="(arr, name) in { ingredients: FORM.ingredients, directions: FORM.directions }">
<h2 class="section-title">{{ name }}</h2>
<ul v-if="name == 'ingredients'" :class="name">
<li v-for="(ingredient, i) in arr">
<input type="text" v-model="arr[i]"/>
</li>
</ul>
<ol :class="name" v-else>
<li v-for="(direction, i) in arr">
<textarea v-model="arr[i]" rows="5"/>
</li>
</ol>
<div class="actions">
<button type="button" class="bi bi-plus-lg" @click="arr.push('')" />
<button type="button" class="bi bi-dash-lg" @click="arr.pop()" :disabled="!arr.length" />
</div>
</div>
<div class="actions">
<button type="submit">{{ FORM.action }}</button>
<button type="button" @click="closeModal">Close</button>
</div>
</form>
</aside>
</Transition>
</template>
<script>
import { ref, computed, watch, nextTick } from "vue";
export default {
setup() {
const INITIAL_RECIPES = {
"Classic Waffles": {
ingredients: [
"2 cups all-purpose flour",
"1 teaspoon salt",
"4 teaspoons baking powder",
"2 tablespoons white sugar",
"2 eggs",
"1 ½ cups warm milk",
"⅓ cup butter, melted",
"1 teaspoon vanilla extract"
],
directions: [
"In a large bowl, mix together flour, salt, baking powder and sugar; set aside. Preheat waffle iron to desired temperature.",
"In a separate bowl, beat the eggs. Stir in the milk, butter and vanilla. Pour the milk mixture into the flour mixture; beat until blended.",
"Ladle the batter into a preheated waffle iron. Cook the waffles until golden and crisp. Serve immediately."
]
},
"Chocolate Oatmeal Cookies": {
ingredients: [
"1 cup all-purpose flour",
"3 tablespoons unsweetened cocoa powder",
"1 teaspoon baking powder",
"½ teaspoon baking soda",
"½ teaspoon salt",
"½ teaspoon ground cinnamon",
"½ cup margarine",
"½ cup brown sugar",
"½ cup white sugar",
"1 egg",
"1 teaspoon vanilla extract",
"1 ¼ cups rolled oats",
"½ cup semisweet chocolate chips"
],
directions: [
"Preheat oven to 350 degrees F (175 degrees C). Grease cookie sheets. Stir together the flour, cocoa, baking powder, baking soda, salt and cinnamon; set aside.",
"In a large bowl, cream together the margarine, brown sugar and white sugar. Beat in the egg and vanilla. Stir in the dry ingredients using a wooden spoon. Mix in the oats and chocolate chips. Drop by tablespoonfuls onto cookie sheets, leaving 2 inches between cookies.",
"Bake for 8 to 10 minutes in the preheated oven, or until lightly browned. Allow cookies to cool on baking sheet for 5 minutes before removing to a wire rack to cool completely."
]
},
"Spatchcock Chicken": {
ingredients: [
"2 (3 1/2) pound whole chickens, wingtips removed",
"2 teaspoons salt",
"1 teaspoon dried tarragon",
"1 teaspoon paprika",
"¼ teaspoon black pepper",
"4 teaspoons olive oil",
"2 lemons, thinly sliced and seeded"
],
directions: [
"Preheat oven to 450 degrees F (230 degrees C). Line a large rimmed baking sheet with foil.",
"Place chicken, breast side down, on a work surface. Starting at the tail end, cut along both sides of backbone with kitchen shears. Remove backbone. Grabbing hold of both sides of the chicken, open it like a book. Turn breast side up. Push down on each side of breast with your hands until you hear it crack. Flatten chicken and transfer to one short end of the prepared baking sheet. Repeat with the second chicken.",
"Combine salt, tarragon, paprika, and pepper in a small bowl. Stir in oil. Run your fingers under chicken skin and rub tarragon paste under skin. Slide lemon slices under skin, in a single layer.",
"Roast until skin is crisp and an instant-read thermometer inserted into thickest part of breast reads 165 degrees F, about 35 minutes. Let stand 5 minutes before cutting each chicken into 8 pieces."
]
}
},
//
KEY = "_SSbit01_recipes",
RECIPES = ref({}),
RECIPES_NAMES = computed(() => Object.keys(RECIPES.value)),
SELECTED = ref("");
try {
if (typeof localStorage === "object" && navigator.cookieEnabled) {
if (localStorage[KEY]) {
const local = JSON.parse(localStorage[KEY]);
RECIPES.value = local.recipes;
SELECTED.value = local.selected;
} else {
RECIPES.value = { ...INITIAL_RECIPES }
SELECTED.value = RECIPES_NAMES.value[0];
localStorage[KEY] = JSON.stringify({
recipes: RECIPES.value,
selected: SELECTED.value
});
}
watch([RECIPES.value, SELECTED], ([recipes, selected]) => {
localStorage[KEY] = JSON.stringify({ recipes, selected })
});
} else {
throw Error("LocalStorage not available");
}
} catch({ message }) {
alert(message);
RECIPES.value = { ...INITIAL_RECIPES }
SELECTED.value = RECIPES_NAMES.value[0];
}
//
//
function deleteRecipe() {
if (confirm(`Are you sure you want to delete the ${SELECTED.value} recipe?`)) {
delete RECIPES.value[SELECTED.value];
SELECTED.value = RECIPES_NAMES.value[0];
}
}
function resetRecipes() {
if (confirm("Are you sure you want to restore default recipes?")) {
for (const i in RECIPES.value) delete RECIPES.value[i];
Object.assign(RECIPES.value, INITIAL_RECIPES);
SELECTED.value = RECIPES_NAMES.value[0];
}
}
//
// Modal
const MODAL_WINDOW = ref(null),
MODAL_OPENED = ref(false),
RECIPE_NAME_INPUT = ref(null),
FORM = ref({
recipe: "",
ingredients: [""],
directions: [""],
action: "add"
});
async function openModal(recipe = "", ingredients = [""], directions = [""], action = "add") {
FORM.value = { recipe, ingredients, directions, action }
MODAL_OPENED.value = true;
document.body.style.overflow = "hidden";
await nextTick();
MODAL_WINDOW.value?.scrollTo(0, 0);
}
function closeModal() {
MODAL_OPENED.value = false;
document.body.style.overflow = "auto";
}
function handleForm() {
const { value: { recipe, ingredients, directions, action } } = FORM;
if (action == "edit") {
delete RECIPES.value[SELECTED.value];
}
if (!RECIPES.value[recipe] || confirm("There's already a recipe with that name. Do you want to replace it?")) {
RECIPES.value[recipe] = {
ingredients: ingredients.filter(i => i != ""),
directions: directions.filter(d => d != "")
}
SELECTED.value = recipe;
closeModal();
}
}
//
return {
RECIPES,
RECIPES_NAMES,
SELECTED,
deleteRecipe,
resetRecipes,
MODAL_WINDOW,
MODAL_OPENED,
RECIPE_NAME_INPUT,
FORM,
openModal,
closeModal,
handleForm
}
}
}
</script>
<style lang="sass">
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css")
body
font-family: Avenir, Helvetica, Arial, sans-serif
background-color: SandyBrown
margin: 0
main
display: grid
gap: 1.5em
color: white
background-color: FireBrick
max-width: 50em
$px: .5em
padding: 1em $px 2em $px
border: medium solid DarkRed
box-shadow: 4px 4px 1rem black
margin: 0 auto
.v-
&enter-active, &leave-active
transition-duration: .2s
&enter-from, &leave-to
opacity: 0
.actions
display: flex
gap: .25em
flex-wrap: wrap
> button
flex: 1
font-size: inherit
text-transform: capitalize
font-variant: small-caps
font-weight: bold
text-shadow: 0 0 4px Black
padding: .5em
border: none
border-radius: 4px
box-shadow: 0 0 4px Black
transition-property: color, background-color, text-shadow
transition-duration: .2s
&:enabled
color: Cornsilk
background-color: rgba(30, 30, 40, .8)
cursor: pointer
&:hover
background-color: rgba(30, 45, 60, .75)
&:active
background-color: rgba(40, 60, 80, .75)
text-shadow: 0 0 2px gold
&:disabled
cursor: not-allowed
color: Black
.box
display: grid
background-color: rgba(0, 0, 0, .75)
padding: 1em .75em
border-radius: 8px
box-shadow: 0 0 4px
ul
display: grid
gap: .5em
padding-left: 1em
margin-top: .5em
margin-bottom: 0
li
line-height: 1.5
&::marker
color: rgb(180, 240, 255)
.section-title
font-variant: small-caps
text-transform: capitalize
text-align: center
background-color: rgba(0, 0, 0, .4)
max-width: max-content
padding: .2em .4em
border: thin solid DarkSlateGray
border-radius: 4px
$mx: auto
margin: 0 $mx .5em $mx
.directions
display: grid
gap: 1em
list-style: none
padding: 0
margin: 0
> li
counter-increment: count
&::before
content: "Step "counter(count)
display: block
text-decoration: underline
color: rgb(180, 240, 255)
font-variant: small-caps
font-size: 1.5em
font-style: italic
margin-bottom: .25rem
#title
text-align: center
text-shadow: 1px 1px 4px black
font-style: italic
text-decoration: underline
text-decoration-style: double
margin: 0
#actions
font-size: 1.4em
#recipes
overflow: auto
width: 100%
font-size: inherit
border: thick solid LightSlateGray
border-radius: 8px
box-shadow: 0 0 4px Black
margin-bottom: .5em
> option
padding: .2em
#modal
position: fixed
z-index: 1
left: 0
top: 0
width: 100%
height: 100%
background-color: rgba(0, 0, 0, .5)
backdrop-filter: blur(.2em)
display: flex
box-sizing: border-box
$px: 0
padding: 1em $px 2.5em $px
overflow: auto
overscroll-behavior: none
&-content
display: grid
gap: 1em
color: white
background-color: rgba(0, 0, 0, .5)
width: 35em
padding: .5em
border: thin solid
border-radius: 6px
margin: auto
.actions
margin-top: 1em
> button
font-size: 1.4em
> .box
> ul
list-style: none
padding: 0
input, textarea
font: inherit
box-sizing: border-box
width: 100%
padding: .4em
border: thin solid transparent
border-radius: 4px
margin: auto
transition: border-color .2s, box-shadow .2s
&:focus
border-color: Blue
box-shadow: 0 0 4px 3px DodgerBlue
outline: none
textarea
resize: vertical
</style>
Also see: Tab Triggers